diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f501e2..47514ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.3.0 * Support of latest Android and iOS * Improvements on example app +* Updates from RandomModderJDK ## 0.2.3 diff --git a/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt b/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt index 467536c..03d550d 100644 --- a/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt +++ b/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt @@ -1,16 +1,29 @@ package com.victorblaess.native_flutter_proxy +import android.content.BroadcastReceiver +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Proxy +import android.net.ProxyInfo +import android.net.Uri +import android.os.Build +import android.util.Log +import androidx.core.content.ContextCompat.getSystemService import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import java.util.LinkedHashMap -class FlutterProxyPlugin : FlutterPlugin, MethodCallHandler { + +class FlutterProxyPlugin : FlutterPlugin, MethodCallHandler, BroadcastReceiver() { private var methodChannel: MethodChannel? = null + private var context: Context? = null private fun setupChannel(messenger: BinaryMessenger) { methodChannel = MethodChannel(messenger, "native_flutter_proxy") @@ -19,25 +32,88 @@ class FlutterProxyPlugin : FlutterPlugin, MethodCallHandler { override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { setupChannel(binding.binaryMessenger) + context = binding.applicationContext + + // initial default proxy, that could not be captured beforehand + val pi = refreshProxyInfo(null) // this does not look consider proxies from PAC + Log.d("ProxyChangeReceiver", "Properties: ${System.getProperty("http.proxyHost")}:${System.getProperty("http.proxyPort")}") + Log.d("ProxyChangeReceiver", "ProxyInfo without intent: ${pi?.host}:${pi?.port}") + + context!!.registerReceiver(this, IntentFilter(Proxy.PROXY_CHANGE_ACTION)) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { methodChannel?.setMethodCallHandler(null) methodChannel = null + context!!.unregisterReceiver(this) + context = null } override fun onMethodCall(call: MethodCall, result: Result) { if (call.method == "getProxySetting") { - result.success(getProxySetting()) + result.success(proxySetting) } else { result.notImplemented() } } - private fun getProxySetting(): Any? { - val map = LinkedHashMap() - map["host"] = System.getProperty("http.proxyHost") - map["port"] = System.getProperty("http.proxyPort") - return map + private val proxySetting: LinkedHashMap = LinkedHashMap() + + override fun onReceive(context: Context, intent: Intent) { + if (Proxy.PROXY_CHANGE_ACTION == intent.action) { + // Handle the proxy change here + Log.d("ProxyChangeReceiver", "Proxy settings changed") + val pi = refreshProxyInfo(intent) + Log.d("ProxyChangeReceiver", "ProxyInfo: ${pi?.host}:${pi?.port}") + methodChannel!!.invokeMethod("proxyChangedCallback", proxySetting) + } + } + + /** + * Get system proxy and update cache with optional intent argument needed for PAC. + */ + private fun refreshProxyInfo(intent: Intent?): ProxyInfo? { + val connectivityManager = getSystemService(context!!,ConnectivityManager::class.java) + var info: ProxyInfo? = connectivityManager!!.defaultProxy + if (info == null) { + proxySetting["host"] = null + proxySetting["port"] = null + return null + } + + // If a proxy is configured using the PAC file use + // Android's injected localhost HTTP proxy. + // + // Android's injected localhost proxy can be accessed using a proxy host + // equal to `localhost` and a proxy port retrieved from intent's 'extras'. + // We cannot take a proxy port from the ProxyInfo object that's exposed by + // the connectivity manager as it's always equal to -1 for cases when PAC + // proxy is configured. + if (info.pacFileUrl != null && info.pacFileUrl !== Uri.EMPTY) { + if (intent == null) { + proxySetting["host"] = null + proxySetting["port"] = null + // PAC proxies are supported only when Intent is present + return null + } + + val extras = intent.extras + if (extras == null) { + proxySetting["host"] = null + proxySetting["port"] = null + return null + } + + info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + extras.getParcelable("android.intent.extra.PROXY_INFO", ProxyInfo::class.java) + } else { + @Suppress("DEPRECATION") + extras.getParcelable("android.intent.extra.PROXY_INFO") as? ProxyInfo + } + } + + proxySetting["host"] = info!!.host + proxySetting["port"] = info.port + return info } } diff --git a/example/lib/app.dart b/example/lib/app.dart index 16af527..43cb23e 100644 --- a/example/lib/app.dart +++ b/example/lib/app.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'data/models/proxy_info.dart'; @@ -6,9 +7,9 @@ import 'view/screens/proxy_screen.dart'; export 'data/models/proxy_info.dart'; class App extends StatelessWidget { - const App({super.key, required this.proxyInfo}); + const App({super.key, required this.proxyInfoListenable}); - final ProxyInfo proxyInfo; + final ValueListenable proxyInfoListenable; @override Widget build(BuildContext context) { @@ -60,7 +61,12 @@ class App extends StatelessWidget { scaffoldBackgroundColor: const Color(0xFFF7EFE6), textTheme: textTheme, ), - home: ProxyScreen(info: proxyInfo), + home: ValueListenableBuilder( + valueListenable: proxyInfoListenable, + builder: (context, info, _) { + return ProxyScreen(info: info); + }, + ), ); } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 0296599..a348c04 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,44 +1,67 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:native_flutter_proxy/native_flutter_proxy.dart'; + import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - bool enabled = false; - String? host; - int? port; - bool applied = false; - String? error; + final proxyInfoNotifier = ValueNotifier( + const ProxyInfo(enabled: false, applied: false), + ); + + Future updateProxyInfo(ProxySetting settings) async { + final enabled = settings.enabled; + final host = settings.host; + final port = settings.port; + var applied = false; + + if (enabled && host != null) { + final proxy = CustomProxy(ipAddress: host, port: port); + proxy.enable(); + applied = true; + debugPrint('====\nProxy enabled\n===='); + } else { + HttpOverrides.global = null; + debugPrint('====\nProxy disabled\n===='); + } + + proxyInfoNotifier.value = ProxyInfo( + enabled: enabled, + applied: applied, + host: host, + port: port, + ); + } + + Future handleError(Object error) async { + HttpOverrides.global = null; + final message = error.toString(); + debugPrint(message); + proxyInfoNotifier.value = ProxyInfo( + enabled: false, + applied: false, + error: message, + ); + } try { - ProxySetting settings = await NativeProxyReader.proxySetting; - enabled = settings.enabled; - host = settings.host; - port = settings.port; + final settings = await NativeProxyReader.proxySetting; + await updateProxyInfo(settings); } catch (e) { - error = e.toString(); - debugPrint(error); + await handleError(e); } - if (enabled && host != null) { - final proxy = CustomProxy(ipAddress: host, port: port); - proxy.enable(); - applied = true; - debugPrint("====\nProxy enabled\n===="); - } else { - debugPrint("====\nProxy disabled\n===="); - } + NativeProxyReader.setProxyChangedCallback((settings) async { + debugPrint('Callback for proxy change was used'); + try { + await updateProxyInfo(settings); + } catch (e) { + await handleError(e); + } + }); - runApp( - App( - proxyInfo: ProxyInfo( - enabled: enabled, - applied: applied, - host: host, - port: port, - error: error, - ), - ), - ); + runApp(App(proxyInfoListenable: proxyInfoNotifier)); } diff --git a/ios/Classes/SwiftFlutterProxyPlugin.swift b/ios/Classes/SwiftFlutterProxyPlugin.swift index 360c8a2..4e55ee4 100644 --- a/ios/Classes/SwiftFlutterProxyPlugin.swift +++ b/ios/Classes/SwiftFlutterProxyPlugin.swift @@ -1,4 +1,6 @@ import Flutter +import Network +import SystemConfiguration import UIKit /** @@ -9,6 +11,16 @@ import UIKit * from Dart code. It provides functionality to retrieve the system proxy settings. */ public class SwiftFlutterProxyPlugin: NSObject, FlutterPlugin { + private var channel: FlutterMethodChannel? + @available(iOS 12.0, *) + private var pathMonitor: NWPathMonitor? + private var pathMonitorQueue: DispatchQueue? + private var reachability: SCNetworkReachability? + private var reachabilityQueue: DispatchQueue? + private var appActiveObserver: NSObjectProtocol? + private var proxyPollTimer: DispatchSourceTimer? + private let proxyStateLock = NSLock() + private var lastProxyKey: String? /** * Registers the plugin with the Flutter plugin registrar. @@ -18,6 +30,8 @@ public class SwiftFlutterProxyPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "native_flutter_proxy", binaryMessenger: registrar.messenger()) let instance = SwiftFlutterProxyPlugin() + instance.channel = channel + instance.startProxyChangeObserver() registrar.addMethodCallDelegate(instance, channel: channel) } @@ -38,6 +52,195 @@ public class SwiftFlutterProxyPlugin: NSObject, FlutterPlugin { } } + deinit { + stopProxyChangeObserver() + } + + private func startProxyChangeObserver() { + if appActiveObserver == nil { + appActiveObserver = NotificationCenter.default.addObserver( + forName: UIApplication.didBecomeActiveNotification, + object: nil, + queue: .main + ) { [weak self] _ in + self?.notifyProxyChanged() + } + } + + seedProxyKey() + startProxyPolling() + + if #available(iOS 12.0, *) { + guard pathMonitor == nil else { + return + } + + let monitor = NWPathMonitor() + pathMonitor = monitor + let queue = DispatchQueue(label: "native_flutter_proxy.pathMonitor") + pathMonitorQueue = queue + monitor.pathUpdateHandler = { [weak self] _ in + self?.notifyProxyChanged() + } + monitor.start(queue: queue) + return + } + + startReachabilityObserver() + } + + private func startReachabilityObserver() { + guard reachability == nil else { + return + } + + guard let reachability = makeReachability() else { + return + } + + self.reachability = reachability + reachabilityQueue = DispatchQueue(label: "native_flutter_proxy.reachability") + + var context = SCNetworkReachabilityContext( + version: 0, + info: Unmanaged.passUnretained(self).toOpaque(), + retain: nil, + release: nil, + copyDescription: nil + ) + + let callback: SCNetworkReachabilityCallBack = { _, _, info in + guard let info = info else { + return + } + let plugin = Unmanaged + .fromOpaque(info) + .takeUnretainedValue() + plugin.notifyProxyChanged() + } + + if !SCNetworkReachabilitySetCallback(reachability, callback, &context) { + self.reachability = nil + reachabilityQueue = nil + return + } + + if let queue = reachabilityQueue, + !SCNetworkReachabilitySetDispatchQueue(reachability, queue) { + SCNetworkReachabilitySetCallback(reachability, nil, nil) + self.reachability = nil + reachabilityQueue = nil + } + } + + private func stopProxyChangeObserver() { + if let appActiveObserver { + NotificationCenter.default.removeObserver(appActiveObserver) + self.appActiveObserver = nil + } + + if #available(iOS 12.0, *) { + if let pathMonitor { + pathMonitor.cancel() + self.pathMonitor = nil + } + pathMonitorQueue = nil + } + + stopProxyPolling() + stopReachabilityObserver() + } + + private func stopReachabilityObserver() { + if let reachability { + SCNetworkReachabilitySetDispatchQueue(reachability, nil) + self.reachability = nil + } + reachabilityQueue = nil + } + + private func makeReachability() -> SCNetworkReachability? { + var address = sockaddr_in() + address.sin_len = UInt8(MemoryLayout.size) + address.sin_family = sa_family_t(AF_INET) + + return withUnsafePointer(to: &address) { pointer in + pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { addrPointer in + SCNetworkReachabilityCreateWithAddress(nil, addrPointer) + } + } + } + + private func startProxyPolling() { + guard proxyPollTimer == nil else { + return + } + + let queue = DispatchQueue(label: "native_flutter_proxy.proxyPoll") + let timer = DispatchSource.makeTimerSource(queue: queue) + timer.schedule(deadline: .now(), repeating: 5.0) + timer.setEventHandler { [weak self] in + self?.notifyProxyChanged() + } + timer.resume() + proxyPollTimer = timer + } + + private func stopProxyPolling() { + if let proxyPollTimer { + proxyPollTimer.cancel() + self.proxyPollTimer = nil + } + } + + private func seedProxyKey() { + let snapshot = proxySettingSnapshot() + proxyStateLock.lock() + lastProxyKey = snapshot.key + proxyStateLock.unlock() + } + + private func notifyProxyChanged() { + let snapshot = proxySettingSnapshot() + proxyStateLock.lock() + let shouldNotify = snapshot.key != lastProxyKey + if shouldNotify { + lastProxyKey = snapshot.key + } + proxyStateLock.unlock() + + guard shouldNotify else { + return + } + DispatchQueue.main.async { [weak self] in + self?.channel?.invokeMethod("proxyChangedCallback", arguments: snapshot.payload) + } + } + + private func proxySettingSnapshot() -> (payload: [String: Any], key: String) { + guard let setting = getProxySetting() as? [String: Any] else { + let payload: [String: Any] = ["host": NSNull(), "port": NSNull()] + return (payload, "|") + } + + let hostValue = setting["host"] as? String + let portValue = setting["port"] + let hostPayload: Any = hostValue ?? NSNull() + let portPayload: Any = portValue ?? NSNull() + let portKey: String + if let portNumber = portValue as? NSNumber { + portKey = portNumber.stringValue + } else if let portInt = portValue as? Int { + portKey = "\(portInt)" + } else if let portString = portValue as? String { + portKey = portString + } else { + portKey = "" + } + let key = "\(hostValue ?? "")|\(portKey)" + return (["host": hostPayload, "port": portPayload], key) + } + /** * Retrieves the system proxy settings. * @@ -59,4 +262,4 @@ public class SwiftFlutterProxyPlugin: NSObject, FlutterPlugin { } return nil } -} \ No newline at end of file +} diff --git a/ios/native_flutter_proxy.podspec b/ios/native_flutter_proxy.podspec index 7095a28..3878c4c 100644 --- a/ios/native_flutter_proxy.podspec +++ b/ios/native_flutter_proxy.podspec @@ -15,6 +15,8 @@ device proxy info. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' + s.frameworks = 'SystemConfiguration' + s.weak_frameworks = 'Network' s.ios.deployment_target = '8.0' # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. diff --git a/lib/src/native_proxy_reader.dart b/lib/src/native_proxy_reader.dart index 188bd6f..a79a187 100644 --- a/lib/src/native_proxy_reader.dart +++ b/lib/src/native_proxy_reader.dart @@ -25,6 +25,21 @@ abstract final class NativeProxyReader { static Future get proxySetting async { return _channel.invokeMapMethod('getProxySetting').then(ProxySetting._fromMap); } + + /// Register callback, when the system proxy is changed. + /// This is the only way to use a proxy specified by PAC. + static void setProxyChangedCallback(Future Function(ProxySetting)? handler) { + _channel.setMethodCallHandler((call) async { + if (handler != null && + call.method == 'proxyChangedCallback' && + call.arguments is Map) { + final map = (call.arguments as Map).cast(); + await handler(ProxySetting._fromMap(map)); + } else { + throw MissingPluginException('notImplemented'); + } + }); + } } /// {@template proxy_setting}