diff --git a/.github/workflows/webview-profiles.yml b/.github/workflows/webview-profiles.yml index dbd900b7..85bb6d99 100644 --- a/.github/workflows/webview-profiles.yml +++ b/.github/workflows/webview-profiles.yml @@ -1,9 +1,9 @@ name: WebView host profiles on: push: - paths: ['internal/webview/**', 'pkg/download/extension_webview*', 'ui/flutter/third_party/**', 'ui/flutter/lib/app/rpc/webview*', 'ui/flutter/tool/webview_profile_check.dart', '.github/workflows/webview-profiles.yml'] + paths: ['go.mod', 'go.sum', 'internal/webview/**', 'pkg/download/extension_webview*', 'ui/flutter/third_party/**', 'ui/flutter/lib/app/rpc/webview*', 'ui/flutter/tool/webview_profile_check.dart', '.github/workflows/webview-profiles.yml'] pull_request: - paths: ['internal/webview/**', 'pkg/download/extension_webview*', 'ui/flutter/third_party/**', 'ui/flutter/lib/app/rpc/webview*', 'ui/flutter/tool/webview_profile_check.dart', '.github/workflows/webview-profiles.yml'] + paths: ['go.mod', 'go.sum', 'internal/webview/**', 'pkg/download/extension_webview*', 'ui/flutter/third_party/**', 'ui/flutter/lib/app/rpc/webview*', 'ui/flutter/tool/webview_profile_check.dart', '.github/workflows/webview-profiles.yml'] workflow_dispatch: jobs: android-plugin: diff --git a/go.mod b/go.mod index c099e88f..c397d41f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/GopeedLab/gopeed go 1.25.4 require ( - github.com/GopeedLab/webview_go v0.0.0-20260913163011-2273ba19d2db + github.com/GopeedLab/webview_go v0.0.0-20260913163948-312792cc8833 github.com/anacrolix/torrent v1.60.1-0.20251217073903-486bcbe758e0 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/bodgit/sevenzip v1.6.1 diff --git a/go.sum b/go.sum index ee70062f..5f1e9d04 100644 --- a/go.sum +++ b/go.sum @@ -23,12 +23,8 @@ filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmG filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/GopeedLab/webview_go v0.0.0-20260423085439-7a2f88b6e9b5 h1:86LVTaNDM/j/uTAD2HedkXNO9LIQlt642VAdbU/0D/M= -github.com/GopeedLab/webview_go v0.0.0-20260423085439-7a2f88b6e9b5/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0= -github.com/GopeedLab/webview_go v0.0.0-20260913162656-5f4deb099005 h1:4jyEJfmP5tpx+qaWqTF6Y04jy4xTTTGWo3l2alh9uZM= -github.com/GopeedLab/webview_go v0.0.0-20260913162656-5f4deb099005/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0= -github.com/GopeedLab/webview_go v0.0.0-20260913163011-2273ba19d2db h1:kZya8+v61mZx7ys0eN1MePyXJPoB1HGRE9O4cValX9M= -github.com/GopeedLab/webview_go v0.0.0-20260913163011-2273ba19d2db/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0= +github.com/GopeedLab/webview_go v0.0.0-20260913163948-312792cc8833 h1:g0jFsuDhGZvALiBr1rxiNZlPC5eb4H9k7KUpkQ//MuI= +github.com/GopeedLab/webview_go v0.0.0-20260913163948-312792cc8833/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= diff --git a/ui/flutter/lib/app/rpc/webview_rpc_service.dart b/ui/flutter/lib/app/rpc/webview_rpc_service.dart index 6fb4b161..df560ef7 100644 --- a/ui/flutter/lib/app/rpc/webview_rpc_service.dart +++ b/ui/flutter/lib/app/rpc/webview_rpc_service.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import '../../core/common/start_config.dart'; @@ -273,6 +274,9 @@ class WebViewRpcService { if (error is WebViewRpcException) { return {'code': error.code, 'message': error.message}; } + if (error is PlatformException && error.code == 'UNAVAILABLE') { + return {'code': error.code, 'message': error.message ?? 'WebView is unavailable'}; + } final code = switch (method) { 'page.goto' => 'NAVIGATION_FAILED', 'page.execute' => 'EVALUATION_FAILED', diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/GopeedProfiles.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/GopeedProfiles.java index da00ca3a..81e13f62 100644 --- a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/GopeedProfiles.java +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/GopeedProfiles.java @@ -10,6 +10,9 @@ import io.flutter.plugin.common.MethodChannel; // Internal host bridge: profile creation and proxy completion precede navigation. final class GopeedProfiles { + private static String appliedProxy; + private static String pendingProxy; + private static final java.util.List pending = new java.util.ArrayList<>(); static void prepare(MethodCall call, MethodChannel.Result result) { if (!WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) || !WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { @@ -24,10 +27,29 @@ final class GopeedProfiles { if (!"http".equals(uri.getScheme()) || !"127.0.0.1".equals(uri.getHost()) || uri.getPort() <= 0) throw new IllegalArgumentException("Invalid host proxy configuration"); ProfileStore.getInstance().getOrCreateProfile(id); + if (proxy.equals(appliedProxy)) { result.success(true); return; } + if (pendingProxy != null) { + if (proxy.equals(pendingProxy)) pending.add(result); + else result.error("UNAVAILABLE", "A different WebView proxy is being configured", null); + return; + } + pendingProxy = proxy; + pending.add(result); ProxyConfig config = new ProxyConfig.Builder().addProxyRule(proxy).removeImplicitRules().build(); - ProxyController.getInstance().setProxyOverride(config, Runnable::run, () -> result.success(true)); + ProxyController.getInstance().setProxyOverride(config, task -> new android.os.Handler(android.os.Looper.getMainLooper()).post(task), () -> { + appliedProxy = proxy; + pendingProxy = null; + java.util.List completed = new java.util.ArrayList<>(pending); + pending.clear(); + for (MethodChannel.Result callback : completed) callback.success(true); + }); } catch (RuntimeException e) { - result.error("UNAVAILABLE", "Unable to configure WebView profile and proxy", null); + if (pendingProxy != null && pending.contains(result)) { + pendingProxy = null; + java.util.List failed = new java.util.ArrayList<>(pending); + pending.clear(); + for (MethodChannel.Result callback : failed) callback.error("UNAVAILABLE", "Unable to configure WebView profile and proxy", null); + } else result.error("UNAVAILABLE", "Unable to configure WebView profile and proxy", null); } } } diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/GopeedProfiles.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/GopeedProfiles.swift index bbc16471..f431fc7b 100644 --- a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/GopeedProfiles.swift +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/GopeedProfiles.swift @@ -5,6 +5,7 @@ import Network import Flutter final class GopeedProfiles { + private static var proxyURLs: [String: String] = [:] private static var stores: [String: WKWebsiteDataStore] = [:] static func store(_ identifier: String) -> WKWebsiteDataStore? { stores[identifier] } @@ -19,11 +20,16 @@ final class GopeedProfiles { result(FlutterError(code: "UNAVAILABLE", message: "Isolated WebView profiles and proxy require macOS 14 / iOS 17 or newer and valid host configuration", details: nil)) return } + if stores[identifier] != nil && proxyURLs[identifier] == proxyURL { + result(true) + return + } let store = stores[identifier] ?? WKWebsiteDataStore(forIdentifier: uuid) let endpoint = nw_endpoint_create_host(host, String(port)) let proxy = nw_proxy_config_create_socksv5(endpoint) store.__proxyConfigurations = [proxy] stores[identifier] = store + proxyURLs[identifier] = proxyURL result(true) } } diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/GopeedProfiles.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/GopeedProfiles.swift index fff25b8c..e2f455ae 100644 --- a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/GopeedProfiles.swift +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/GopeedProfiles.swift @@ -5,6 +5,7 @@ import Network import FlutterMacOS final class GopeedProfiles { + private static var proxyURLs: [String: String] = [:] private static var stores: [String: WKWebsiteDataStore] = [:] static func store(_ identifier: String) -> WKWebsiteDataStore? { stores[identifier] } @@ -19,11 +20,16 @@ final class GopeedProfiles { result(FlutterError(code: "UNAVAILABLE", message: "Isolated WebView profiles and proxy require macOS 14 / iOS 17 or newer and valid host configuration", details: nil)) return } + if stores[identifier] != nil && proxyURLs[identifier] == proxyURL { + result(true) + return + } let store = stores[identifier] ?? WKWebsiteDataStore(forIdentifier: uuid) let endpoint = nw_endpoint_create_host(host, String(port)) let proxy = nw_proxy_config_create_socksv5(endpoint) store.__proxyConfigurations = [proxy] stores[identifier] = store + proxyURLs[identifier] = proxyURL result(true) } } diff --git a/ui/flutter/tool/webview_profile_check.dart b/ui/flutter/tool/webview_profile_check.dart index ac416c31..94ae2d92 100644 --- a/ui/flutter/tool/webview_profile_check.dart +++ b/ui/flutter/tool/webview_profile_check.dart @@ -97,6 +97,7 @@ Future checkProfiles() async { final target = WebUri('https://profiles.invalid/'); final pages = []; Future open(WebViewProfile profile) async { + await profile.prepare(proxyUrl); final ready = Completer(); final page = HeadlessInAppWebView( onReceivedServerTrustAuthRequest: (_, _) async =>