Skip to content

Commit 67826b3

Browse files
author
sylar
committed
fix: address Flutter proxy and Windows review feedback
1 parent 1a58637 commit 67826b3

20 files changed

Lines changed: 625 additions & 89 deletions

File tree

examples/flutter/RunAnywhereAI/lib/core/models/proxy_settings.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ enum ProxyScheme {
1818

1919
String get wireValue => name;
2020

21+
bool get isSupportedInExampleApp => this != ProxyScheme.socks5;
22+
23+
static List<ProxyScheme> get supportedValues =>
24+
values.where((scheme) => scheme.isSupportedInExampleApp).toList();
25+
2126
static ProxyScheme fromWireValue(String? value) {
2227
return ProxyScheme.values.firstWhere(
2328
(scheme) => scheme.wireValue == value,
@@ -54,6 +59,7 @@ class ProxySettings {
5459
ProxyScheme? scheme,
5560
String? host,
5661
int? port,
62+
bool clearPort = false,
5763
String? username,
5864
String? password,
5965
bool? bypassLocal,
@@ -62,7 +68,7 @@ class ProxySettings {
6268
enabled: enabled ?? this.enabled,
6369
scheme: scheme ?? this.scheme,
6470
host: host ?? this.host,
65-
port: port ?? this.port,
71+
port: clearPort ? null : port ?? this.port,
6672
username: username ?? this.username,
6773
password: password ?? this.password,
6874
bypassLocal: bypassLocal ?? this.bypassLocal,

examples/flutter/RunAnywhereAI/lib/core/services/example_http_service.dart

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:convert';
22
import 'dart:io';
33

44
import 'package:http/http.dart' as http;
5+
import 'package:flutter/foundation.dart' show visibleForTesting;
56
import 'package:http/io_client.dart';
67
import 'package:runanywhere_ai/core/models/proxy_settings.dart';
78
import 'package:runanywhere_ai/core/services/proxy_settings_service.dart';
@@ -67,20 +68,22 @@ class ExampleHttpService {
6768
Uri targetUri,
6869
) async {
6970
final settings = await ProxySettingsService.shared.load(scope);
70-
final client = HttpClient()..connectionTimeout = const Duration(seconds: 15);
71+
final client = HttpClient()
72+
..connectionTimeout = const Duration(seconds: 15);
7173

7274
if (!settings.enabled ||
7375
!settings.isComplete ||
76+
!settings.scheme.isSupportedInExampleApp ||
7477
!_shouldUseProxy(settings, targetUri.host)) {
7578
client.findProxy = (_) => 'DIRECT';
7679
return client;
7780
}
7881

79-
final proxyDirective = switch (settings.scheme) {
80-
ProxyScheme.socks5 => 'SOCKS5 ${settings.host.trim()}:${settings.port}',
81-
ProxyScheme.http || ProxyScheme.https =>
82-
'PROXY ${settings.host.trim()}:${settings.port}',
83-
};
82+
final proxyDirective = _proxyDirectiveForSettings(settings);
83+
if (proxyDirective == null) {
84+
client.findProxy = (_) => 'DIRECT';
85+
return client;
86+
}
8487

8588
client.findProxy = (_) => proxyDirective;
8689

@@ -118,4 +121,24 @@ class ExampleHttpService {
118121
normalized != '127.0.0.1' &&
119122
normalized != '::1';
120123
}
124+
125+
static String? _proxyDirectiveForSettings(ProxySettings settings) {
126+
if (!settings.scheme.isSupportedInExampleApp ||
127+
settings.port == null ||
128+
settings.host.trim().isEmpty) {
129+
return null;
130+
}
131+
132+
return switch (settings.scheme) {
133+
ProxyScheme.http ||
134+
ProxyScheme.https =>
135+
'PROXY ${settings.host.trim()}:${settings.port}',
136+
ProxyScheme.socks5 => null,
137+
};
138+
}
139+
140+
@visibleForTesting
141+
static String? proxyDirectiveForTesting(ProxySettings settings) {
142+
return _proxyDirectiveForSettings(settings);
143+
}
121144
}

examples/flutter/RunAnywhereAI/lib/core/services/proxy_settings_service.dart

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,22 @@ class ProxySettingsService {
2828

2929
Future<ProxySettings> load(ProxyScope scope) async {
3030
final prefs = await SharedPreferences.getInstance();
31-
final username =
32-
await KeychainHelper.loadString(_usernameKey(scope)) ?? '';
33-
final password =
34-
await KeychainHelper.loadString(_passwordKey(scope)) ?? '';
31+
final username = await KeychainHelper.loadString(_usernameKey(scope)) ?? '';
32+
final password = await KeychainHelper.loadString(_passwordKey(scope)) ?? '';
3533

3634
final enabled = prefs.getBool(_enabledKey(scope));
3735
final scheme = prefs.getString(_schemeKey(scope));
3836
final host = prefs.getString(_hostKey(scope));
3937
final port = prefs.getInt(_portKey(scope));
4038
final bypassLocal = prefs.getBool(_bypassLocalKey(scope));
39+
final hasScopedPreferences = enabled != null ||
40+
scheme != null ||
41+
host != null ||
42+
port != null ||
43+
bypassLocal != null;
4144

4245
ProxySettings settings;
43-
if (scope == ProxyScope.general &&
44-
enabled == null &&
45-
scheme == null &&
46-
host == null &&
47-
port == null &&
48-
bypassLocal == null &&
49-
username.isEmpty &&
50-
password.isEmpty) {
46+
if (scope == ProxyScope.general && !hasScopedPreferences) {
5147
settings = await _loadLegacyGeneralSettings(prefs);
5248
} else {
5349
settings = ProxySettings(
@@ -70,6 +66,12 @@ class ProxySettingsService {
7066
return const ProxySettingsValidationResult.valid();
7167
}
7268

69+
if (!settings.scheme.isSupportedInExampleApp) {
70+
return const ProxySettingsValidationResult.invalid(
71+
'SOCKS5 is temporarily disabled in the Flutter example app.',
72+
);
73+
}
74+
7375
if (settings.host.trim().isEmpty) {
7476
return const ProxySettingsValidationResult.invalid(
7577
'Proxy host is required.',
@@ -142,6 +144,13 @@ class ProxySettingsService {
142144
await prefs.remove(_hostKey(scope));
143145
await prefs.remove(_portKey(scope));
144146
await prefs.remove(_bypassLocalKey(scope));
147+
if (scope == ProxyScope.general) {
148+
await prefs.remove(PreferenceKeys.proxyEnabled);
149+
await prefs.remove(PreferenceKeys.proxyScheme);
150+
await prefs.remove(PreferenceKeys.proxyHost);
151+
await prefs.remove(PreferenceKeys.proxyPort);
152+
await prefs.remove(PreferenceKeys.proxyBypassLocal);
153+
}
145154
await KeychainHelper.delete(_usernameKey(scope));
146155
await KeychainHelper.delete(_passwordKey(scope));
147156
_current[scope] = const ProxySettings();

examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,11 @@ class _CombinedSettingsViewState extends State<CombinedSettingsView> {
9898
final prefs = await SharedPreferences.getInstance();
9999
if (mounted) {
100100
setState(() {
101-
_temperature = prefs.getDouble(PreferenceKeys.defaultTemperature) ?? 0.7;
101+
_temperature =
102+
prefs.getDouble(PreferenceKeys.defaultTemperature) ?? 0.7;
102103
_maxTokens = prefs.getInt(PreferenceKeys.defaultMaxTokens) ?? 1000;
103-
_systemPrompt = prefs.getString(PreferenceKeys.defaultSystemPrompt) ?? '';
104+
_systemPrompt =
105+
prefs.getString(PreferenceKeys.defaultSystemPrompt) ?? '';
104106
_systemPromptController.text = _systemPrompt;
105107
});
106108
}
@@ -138,24 +140,28 @@ class _CombinedSettingsViewState extends State<CombinedSettingsView> {
138140

139141
Future<void> _loadProxyConfiguration(ProxyScope scope) async {
140142
final settings = await ProxySettingsService.shared.load(scope);
143+
final selectedScheme = settings.scheme.isSupportedInExampleApp
144+
? settings.scheme
145+
: ProxyScheme.http;
146+
final isConfigured = settings.enabled &&
147+
settings.isComplete &&
148+
settings.scheme.isSupportedInExampleApp;
141149
if (mounted) {
142150
setState(() {
143151
switch (scope) {
144152
case ProxyScope.general:
145-
_generalProxyScheme = settings.scheme.wireValue;
153+
_generalProxyScheme = selectedScheme.wireValue;
146154
_generalProxyHost = settings.host;
147155
_generalProxyPort = settings.port;
148156
_generalProxyBypassLocal = settings.bypassLocal;
149-
_isGeneralProxyConfigured =
150-
settings.enabled && settings.isComplete;
157+
_isGeneralProxyConfigured = isConfigured;
151158
break;
152159
case ProxyScope.download:
153-
_downloadProxyScheme = settings.scheme.wireValue;
160+
_downloadProxyScheme = selectedScheme.wireValue;
154161
_downloadProxyHost = settings.host;
155162
_downloadProxyPort = settings.port;
156163
_downloadProxyBypassLocal = settings.bypassLocal;
157-
_isDownloadProxyConfigured =
158-
settings.enabled && settings.isComplete;
164+
_isDownloadProxyConfigured = isConfigured;
159165
break;
160166
}
161167
});
@@ -240,8 +246,11 @@ class _CombinedSettingsViewState extends State<CombinedSettingsView> {
240246
TextEditingController(text: currentSettings.username);
241247
final passwordController =
242248
TextEditingController(text: currentSettings.password);
243-
var proxyEnabled = currentSettings.enabled;
244-
var selectedScheme = currentSettings.scheme;
249+
var proxyEnabled = currentSettings.enabled &&
250+
currentSettings.scheme.isSupportedInExampleApp;
251+
var selectedScheme = currentSettings.scheme.isSupportedInExampleApp
252+
? currentSettings.scheme
253+
: ProxyScheme.http;
245254
var bypassLocal = currentSettings.bypassLocal;
246255
var showPassword = false;
247256

@@ -274,7 +283,7 @@ class _CombinedSettingsViewState extends State<CombinedSettingsView> {
274283
labelText: 'Protocol',
275284
border: OutlineInputBorder(),
276285
),
277-
items: ProxyScheme.values
286+
items: ProxyScheme.supportedValues
278287
.map(
279288
(scheme) => DropdownMenuItem(
280289
value: scheme,
@@ -340,7 +349,8 @@ class _CombinedSettingsViewState extends State<CombinedSettingsView> {
340349
SwitchListTile(
341350
contentPadding: EdgeInsets.zero,
342351
title: const Text('Bypass localhost'),
343-
subtitle: const Text('Skip proxy for localhost, 127.0.0.1, and ::1'),
352+
subtitle: const Text(
353+
'Skip proxy for localhost, 127.0.0.1, and ::1'),
344354
value: bypassLocal,
345355
onChanged: (value) {
346356
setDialogState(() {
@@ -918,8 +928,8 @@ class _CombinedSettingsViewState extends State<CombinedSettingsView> {
918928
),
919929
Text(
920930
'${viewModel.registeredTools.length}',
921-
style: AppTypography.subheadlineSemibold(context)
922-
.copyWith(
931+
style:
932+
AppTypography.subheadlineSemibold(context).copyWith(
923933
color: AppColors.primaryAccent,
924934
),
925935
),
@@ -1053,12 +1063,10 @@ class _CombinedSettingsViewState extends State<CombinedSettingsView> {
10531063
final scheme = scope == ProxyScope.general
10541064
? _generalProxyScheme
10551065
: _downloadProxyScheme;
1056-
final host = scope == ProxyScope.general
1057-
? _generalProxyHost
1058-
: _downloadProxyHost;
1059-
final port = scope == ProxyScope.general
1060-
? _generalProxyPort
1061-
: _downloadProxyPort;
1066+
final host =
1067+
scope == ProxyScope.general ? _generalProxyHost : _downloadProxyHost;
1068+
final port =
1069+
scope == ProxyScope.general ? _generalProxyPort : _downloadProxyPort;
10621070
final bypassLocal = scope == ProxyScope.general
10631071
? _generalProxyBypassLocal
10641072
: _downloadProxyBypassLocal;

examples/flutter/RunAnywhereAI/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ dependencies:
3333
shared_preferences: ^2.2.2
3434
# Secure storage (iOS Keychain, Android Keystore)
3535
flutter_secure_storage: ^9.0.0
36+
flutter_secure_storage_windows:
37+
path: vendor/flutter_secure_storage_windows
3638
# File system access
3739
path_provider: ^2.1.0
3840
# UUID generation
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:runanywhere_ai/core/models/proxy_settings.dart';
3+
4+
void main() {
5+
test('supported proxy scheme values exclude SOCKS5 while disabled', () {
6+
expect(ProxyScheme.supportedValues, isNot(contains(ProxyScheme.socks5)));
7+
expect(ProxyScheme.supportedValues, contains(ProxyScheme.http));
8+
expect(ProxyScheme.supportedValues, contains(ProxyScheme.https));
9+
});
10+
11+
test('copyWith can explicitly clear the saved port', () {
12+
const settings = ProxySettings(
13+
enabled: true,
14+
scheme: ProxyScheme.http,
15+
host: 'proxy.local',
16+
port: 8080,
17+
);
18+
19+
final updated = settings.copyWith(clearPort: true);
20+
21+
expect(updated.port, isNull);
22+
expect(updated.host, 'proxy.local');
23+
expect(updated.enabled, isTrue);
24+
});
25+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:runanywhere_ai/core/models/proxy_settings.dart';
3+
import 'package:runanywhere_ai/core/services/example_http_service.dart';
4+
5+
void main() {
6+
test('builds an HTTP proxy directive for supported schemes', () {
7+
const settings = ProxySettings(
8+
enabled: true,
9+
scheme: ProxyScheme.http,
10+
host: 'proxy.local',
11+
port: 8080,
12+
);
13+
14+
expect(
15+
ExampleHttpService.proxyDirectiveForTesting(settings),
16+
'PROXY proxy.local:8080',
17+
);
18+
});
19+
20+
test('does not build a proxy directive for SOCKS5 while disabled', () {
21+
const settings = ProxySettings(
22+
enabled: true,
23+
scheme: ProxyScheme.socks5,
24+
host: 'proxy.local',
25+
port: 1080,
26+
);
27+
28+
expect(ExampleHttpService.proxyDirectiveForTesting(settings), isNull);
29+
});
30+
}

0 commit comments

Comments
 (0)