diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index 74bc2ff9..03e4a350 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -17,6 +17,7 @@ class SettingsModel { this.historyRetentionPeriod = HistoryRetentionPeriod.oneWeek, this.workspaceFolderPath, this.isSSLDisabled = false, + this.proxySettings = const ProxySettings(), }); final bool isDark; @@ -32,6 +33,9 @@ class SettingsModel { final String? workspaceFolderPath; final bool isSSLDisabled; + // Proxy settings + final ProxySettings proxySettings; + SettingsModel copyWith({ bool? isDark, bool? alwaysShowCollectionPaneScrollbar, @@ -45,15 +49,16 @@ class SettingsModel { HistoryRetentionPeriod? historyRetentionPeriod, String? workspaceFolderPath, bool? isSSLDisabled, + ProxySettings? proxySettings, }) { return SettingsModel( isDark: isDark ?? this.isDark, alwaysShowCollectionPaneScrollbar: alwaysShowCollectionPaneScrollbar ?? this.alwaysShowCollectionPaneScrollbar, size: size ?? this.size, + offset: offset ?? this.offset, defaultUriScheme: defaultUriScheme ?? this.defaultUriScheme, defaultCodeGenLang: defaultCodeGenLang ?? this.defaultCodeGenLang, - offset: offset ?? this.offset, saveResponses: saveResponses ?? this.saveResponses, promptBeforeClosing: promptBeforeClosing ?? this.promptBeforeClosing, activeEnvironmentId: activeEnvironmentId ?? this.activeEnvironmentId, @@ -61,6 +66,7 @@ class SettingsModel { historyRetentionPeriod ?? this.historyRetentionPeriod, workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled, + proxySettings: proxySettings ?? this.proxySettings, ); } @@ -71,15 +77,16 @@ class SettingsModel { isDark: isDark, alwaysShowCollectionPaneScrollbar: alwaysShowCollectionPaneScrollbar, size: size, + offset: offset, defaultUriScheme: defaultUriScheme, defaultCodeGenLang: defaultCodeGenLang, - offset: offset, saveResponses: saveResponses, promptBeforeClosing: promptBeforeClosing, activeEnvironmentId: activeEnvironmentId, historyRetentionPeriod: historyRetentionPeriod, workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, + proxySettings: proxySettings, ); } @@ -151,6 +158,7 @@ class SettingsModel { historyRetentionPeriod ?? HistoryRetentionPeriod.oneWeek, workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, + proxySettings: ProxySettings.fromJson(data["proxySettings"]), ); } @@ -170,6 +178,7 @@ class SettingsModel { "historyRetentionPeriod": historyRetentionPeriod.name, "workspaceFolderPath": workspaceFolderPath, "isSSLDisabled": isSSLDisabled, + "proxySettings": proxySettings, }; } @@ -194,7 +203,8 @@ class SettingsModel { other.activeEnvironmentId == activeEnvironmentId && other.historyRetentionPeriod == historyRetentionPeriod && other.workspaceFolderPath == workspaceFolderPath && - other.isSSLDisabled == isSSLDisabled; + other.isSSLDisabled == isSSLDisabled && + other.proxySettings == proxySettings; } @override @@ -213,6 +223,7 @@ class SettingsModel { historyRetentionPeriod, workspaceFolderPath, isSSLDisabled, + proxySettings, ); } } diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 2fb83077..8704c970 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -299,6 +299,7 @@ class CollectionStateNotifier substitutedHttpRequestModel, defaultUriScheme: defaultUriScheme, noSSL: noSSL, + proxySettings: ref.read(settingsProvider).proxySettings, ); late final RequestModel newRequestModel; diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index 6b64343a..7825d786 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -33,20 +33,22 @@ class ThemeStateNotifier extends StateNotifier { HistoryRetentionPeriod? historyRetentionPeriod, String? workspaceFolderPath, bool? isSSLDisabled, + ProxySettings? proxySettings, }) async { state = state.copyWith( - isDark: isDark, - alwaysShowCollectionPaneScrollbar: alwaysShowCollectionPaneScrollbar, - size: size, - offset: offset, - defaultUriScheme: defaultUriScheme, - defaultCodeGenLang: defaultCodeGenLang, - saveResponses: saveResponses, - promptBeforeClosing: promptBeforeClosing, - activeEnvironmentId: activeEnvironmentId, - historyRetentionPeriod: historyRetentionPeriod, - workspaceFolderPath: workspaceFolderPath, - isSSLDisabled: isSSLDisabled, + isDark: isDark ?? state.isDark, + alwaysShowCollectionPaneScrollbar: alwaysShowCollectionPaneScrollbar ?? state.alwaysShowCollectionPaneScrollbar, + size: size ?? state.size, + offset: offset ?? state.offset, + defaultUriScheme: defaultUriScheme ?? state.defaultUriScheme, + defaultCodeGenLang: defaultCodeGenLang ?? state.defaultCodeGenLang, + saveResponses: saveResponses ?? state.saveResponses, + promptBeforeClosing: promptBeforeClosing ?? state.promptBeforeClosing, + activeEnvironmentId: activeEnvironmentId ?? state.activeEnvironmentId, + historyRetentionPeriod: historyRetentionPeriod ?? state.historyRetentionPeriod, + workspaceFolderPath: workspaceFolderPath ?? state.workspaceFolderPath, + isSSLDisabled: isSSLDisabled ?? state.isSSLDisabled, + proxySettings: proxySettings ?? state.proxySettings, ); await setSettingsToSharedPrefs(state); } diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index eb4b6008..bf3ca7eb 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -7,6 +7,7 @@ import '../services/services.dart'; import '../utils/utils.dart'; import '../widgets/widgets.dart'; import '../consts.dart'; +import 'dart:developer' as developer; class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); @@ -50,6 +51,75 @@ class SettingsPage extends ConsumerWidget { ref.read(settingsProvider.notifier).update(isDark: value); }, ), + // Proxy Settings Section + SwitchListTile( + hoverColor: kColorTransparent, + title: const Text('Enable Proxy'), + subtitle: const Text('Configure HTTP proxy settings'), + value: ref.watch(settingsProvider.select((settings) => settings.isProxyEnabled)), + onChanged: (bool? value) { + if (value != null) { + developer.log('Toggling proxy settings', name: 'settings_page', error: 'New value: $value'); + ref.read(settingsProvider.notifier).update( + isProxyEnabled: value, + proxyHost: value ? settings.proxyHost : '', + proxyPort: value ? settings.proxyPort : '', + proxyUsername: value ? settings.proxyUsername : null, + proxyPassword: value ? settings.proxyPassword : null, + ); + developer.log('Proxy settings updated', name: 'settings_page'); + } + }, + ), + if (ref.watch(settingsProvider.select((settings) => settings.isProxyEnabled))) ...[ + ListTile( + title: TextField( + decoration: const InputDecoration( + labelText: 'Proxy Host', + hintText: 'e.g., localhost', + ), + controller: TextEditingController(text: settings.proxyHost), + onChanged: (value) { + ref.read(settingsProvider.notifier).update(proxyHost: value); + }, + ), + ), + ListTile( + title: TextField( + decoration: const InputDecoration( + labelText: 'Proxy Port', + hintText: 'e.g., 8080', + ), + controller: TextEditingController(text: settings.proxyPort), + onChanged: (value) { + ref.read(settingsProvider.notifier).update(proxyPort: value); + }, + ), + ), + ListTile( + title: TextField( + decoration: const InputDecoration( + labelText: 'Username (Optional)', + ), + controller: TextEditingController(text: settings.proxyUsername), + onChanged: (value) { + ref.read(settingsProvider.notifier).update(proxyUsername: value); + }, + ), + ), + ListTile( + title: TextField( + decoration: const InputDecoration( + labelText: 'Password (Optional)', + ), + obscureText: true, + controller: TextEditingController(text: settings.proxyPassword), + onChanged: (value) { + ref.read(settingsProvider.notifier).update(proxyPassword: value); + }, + ), + ), + ], SwitchListTile( hoverColor: kColorTransparent, title: const Text('Collection Pane Scrollbar Visiblity'), diff --git a/packages/apidash_core/lib/models/models.dart b/packages/apidash_core/lib/models/models.dart index a33c6fdd..1a4998b1 100644 --- a/packages/apidash_core/lib/models/models.dart +++ b/packages/apidash_core/lib/models/models.dart @@ -1,2 +1,3 @@ export 'http_request_model.dart'; export 'http_response_model.dart'; +export 'proxy_settings_model.dart'; diff --git a/packages/apidash_core/lib/models/proxy_settings_model.dart b/packages/apidash_core/lib/models/proxy_settings_model.dart new file mode 100644 index 00000000..e8801031 --- /dev/null +++ b/packages/apidash_core/lib/models/proxy_settings_model.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'proxy_settings_model.freezed.dart'; +part 'proxy_settings_model.g.dart'; + +@freezed +class ProxySettings with _$ProxySettings { + const factory ProxySettings({ + @Default('') String host, + @Default('') String port, + String? username, + String? password, + }) = _ProxySettings; + + factory ProxySettings.fromJson(Map json) => + _$ProxySettingsFromJson(json); +} diff --git a/packages/apidash_core/lib/models/proxy_settings_model.freezed.dart b/packages/apidash_core/lib/models/proxy_settings_model.freezed.dart new file mode 100644 index 00000000..bc8b0a82 --- /dev/null +++ b/packages/apidash_core/lib/models/proxy_settings_model.freezed.dart @@ -0,0 +1,221 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'proxy_settings_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ProxySettings _$ProxySettingsFromJson(Map json) { + return _ProxySettings.fromJson(json); +} + +/// @nodoc +mixin _$ProxySettings { + String get host => throw _privateConstructorUsedError; + String get port => throw _privateConstructorUsedError; + String? get username => throw _privateConstructorUsedError; + String? get password => throw _privateConstructorUsedError; + + /// Serializes this ProxySettings to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ProxySettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProxySettingsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProxySettingsCopyWith<$Res> { + factory $ProxySettingsCopyWith( + ProxySettings value, $Res Function(ProxySettings) then) = + _$ProxySettingsCopyWithImpl<$Res, ProxySettings>; + @useResult + $Res call({String host, String port, String? username, String? password}); +} + +/// @nodoc +class _$ProxySettingsCopyWithImpl<$Res, $Val extends ProxySettings> + implements $ProxySettingsCopyWith<$Res> { + _$ProxySettingsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProxySettings + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? host = null, + Object? port = null, + Object? username = freezed, + Object? password = freezed, + }) { + return _then(_value.copyWith( + host: null == host + ? _value.host + : host // ignore: cast_nullable_to_non_nullable + as String, + port: null == port + ? _value.port + : port // ignore: cast_nullable_to_non_nullable + as String, + username: freezed == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String?, + password: freezed == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ProxySettingsImplCopyWith<$Res> + implements $ProxySettingsCopyWith<$Res> { + factory _$$ProxySettingsImplCopyWith( + _$ProxySettingsImpl value, $Res Function(_$ProxySettingsImpl) then) = + __$$ProxySettingsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String host, String port, String? username, String? password}); +} + +/// @nodoc +class __$$ProxySettingsImplCopyWithImpl<$Res> + extends _$ProxySettingsCopyWithImpl<$Res, _$ProxySettingsImpl> + implements _$$ProxySettingsImplCopyWith<$Res> { + __$$ProxySettingsImplCopyWithImpl( + _$ProxySettingsImpl _value, $Res Function(_$ProxySettingsImpl) _then) + : super(_value, _then); + + /// Create a copy of ProxySettings + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? host = null, + Object? port = null, + Object? username = freezed, + Object? password = freezed, + }) { + return _then(_$ProxySettingsImpl( + host: null == host + ? _value.host + : host // ignore: cast_nullable_to_non_nullable + as String, + port: null == port + ? _value.port + : port // ignore: cast_nullable_to_non_nullable + as String, + username: freezed == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String?, + password: freezed == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ProxySettingsImpl implements _ProxySettings { + const _$ProxySettingsImpl( + {this.host = '', this.port = '', this.username, this.password}); + + factory _$ProxySettingsImpl.fromJson(Map json) => + _$$ProxySettingsImplFromJson(json); + + @override + @JsonKey() + final String host; + @override + @JsonKey() + final String port; + @override + final String? username; + @override + final String? password; + + @override + String toString() { + return 'ProxySettings(host: $host, port: $port, username: $username, password: $password)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProxySettingsImpl && + (identical(other.host, host) || other.host == host) && + (identical(other.port, port) || other.port == port) && + (identical(other.username, username) || + other.username == username) && + (identical(other.password, password) || + other.password == password)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, host, port, username, password); + + /// Create a copy of ProxySettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProxySettingsImplCopyWith<_$ProxySettingsImpl> get copyWith => + __$$ProxySettingsImplCopyWithImpl<_$ProxySettingsImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ProxySettingsImplToJson( + this, + ); + } +} + +abstract class _ProxySettings implements ProxySettings { + const factory _ProxySettings( + {final String host, + final String port, + final String? username, + final String? password}) = _$ProxySettingsImpl; + + factory _ProxySettings.fromJson(Map json) = + _$ProxySettingsImpl.fromJson; + + @override + String get host; + @override + String get port; + @override + String? get username; + @override + String? get password; + + /// Create a copy of ProxySettings + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProxySettingsImplCopyWith<_$ProxySettingsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/apidash_core/lib/models/proxy_settings_model.g.dart b/packages/apidash_core/lib/models/proxy_settings_model.g.dart new file mode 100644 index 00000000..383e9976 --- /dev/null +++ b/packages/apidash_core/lib/models/proxy_settings_model.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'proxy_settings_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ProxySettingsImpl _$$ProxySettingsImplFromJson(Map json) => + _$ProxySettingsImpl( + host: json['host'] as String? ?? '', + port: json['port'] as String? ?? '', + username: json['username'] as String?, + password: json['password'] as String?, + ); + +Map _$$ProxySettingsImplToJson(_$ProxySettingsImpl instance) => + { + 'host': instance.host, + 'port': instance.port, + 'username': instance.username, + 'password': instance.password, + }; diff --git a/packages/apidash_core/lib/services/http_client_manager.dart b/packages/apidash_core/lib/services/http_client_manager.dart index bec23214..4f9896e0 100644 --- a/packages/apidash_core/lib/services/http_client_manager.dart +++ b/packages/apidash_core/lib/services/http_client_manager.dart @@ -3,11 +3,38 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:http/io_client.dart'; +import 'package:apidash_core/models/models.dart'; + +http.Client createCustomHttpClient({ + bool noSSL = false, + ProxySettings? proxySettings, +}) { + if (kIsWeb) { + return http.Client(); + } + + var ioClient = HttpClient(); + + // Configure SSL + if (noSSL) { + ioClient.badCertificateCallback = (X509Certificate cert, String host, int port) => true; + } + + // Configure proxy if enabled + if (proxySettings != null) { + // Set proxy server + ioClient.findProxy = (uri) { + return 'PROXY ${proxySettings.host}:${proxySettings.port}'; + }; + + // Configure proxy authentication if credentials are provided + if (proxySettings.username != null && proxySettings.password != null) { + ioClient.authenticate = (Uri url, String scheme, String? realm) async { + return true; + }; + } + } -http.Client createHttpClientWithNoSSL() { - var ioClient = HttpClient() - ..badCertificateCallback = - (X509Certificate cert, String host, int port) => true; return IOClient(ioClient); } @@ -26,9 +53,13 @@ class HttpClientManager { http.Client createClient( String requestId, { bool noSSL = false, + ProxySettings? proxySettings, }) { - final client = - (noSSL && !kIsWeb) ? createHttpClientWithNoSSL() : http.Client(); + final client = createCustomHttpClient( + noSSL: noSSL, + proxySettings: proxySettings, + ); + _clients[requestId] = client; return client; } diff --git a/packages/apidash_core/lib/services/http_service.dart b/packages/apidash_core/lib/services/http_service.dart index 0bbc0501..2220c7d1 100644 --- a/packages/apidash_core/lib/services/http_service.dart +++ b/packages/apidash_core/lib/services/http_service.dart @@ -16,9 +16,14 @@ Future<(HttpResponse?, Duration?, String?)> request( HttpRequestModel requestModel, { SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, bool noSSL = false, + ProxySettings? proxySettings, }) async { final clientManager = HttpClientManager(); - final client = clientManager.createClient(requestId, noSSL: noSSL); + final client = clientManager.createClient( + requestId, + noSSL: noSSL, + proxySettings: proxySettings, + ); (Uri?, String?) uriRec = getValidRequestUri( requestModel.url,