mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 08:30:29 +00:00
fix(webview): release retained macOS views before profile removal
This commit is contained in:
1 parent
311e002e55
commit
7e49b678f2
4 files changed
+78
No files matched your search
@@ -4,11 +4,13 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -16,6 +18,59 @@ import (
|
||||
"github.com/armon/go-socks5"
|
||||
)
|
||||
|
||||
func TestRejectedProxyRequests(t *testing.T) {
|
||||
secret := "upstream-password"
|
||||
for _, tc := range []struct {
|
||||
name, method, target string
|
||||
status int
|
||||
}{
|
||||
{"relative URL", "GET", "/relative", 400},
|
||||
{"unsupported URL", "GET", "ftp://example.com/file", 400},
|
||||
{"invalid CONNECT", "CONNECT", "example.com", 400},
|
||||
{"HTTP selection failure", "GET", "http://example.com/", 502},
|
||||
{"CONNECT selection failure", "CONNECT", "example.com:443", 502},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := &Server{selectProxy: func(*http.Request) (*url.URL, error) { return nil, errors.New(secret) }}
|
||||
s.transport = &http.Transport{Proxy: s.selectProxy}
|
||||
defer s.transport.CloseIdleConnections()
|
||||
response := httptest.NewRecorder()
|
||||
s.ServeHTTP(response, httptest.NewRequest(tc.method, tc.target, nil))
|
||||
if response.Code != tc.status || strings.Contains(response.Body.String(), secret) {
|
||||
t.Fatalf("unexpected proxy rejection: %d %q", response.Code, response.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunnelRejectsUnsafeUpstreams(t *testing.T) {
|
||||
rejected := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "authentication required", http.StatusProxyAuthRequired)
|
||||
}))
|
||||
defer rejected.Close()
|
||||
secure := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Error("untrusted TLS proxy received a request")
|
||||
}))
|
||||
defer secure.Close()
|
||||
for _, upstream := range []string{"ftp://127.0.0.1", rejected.URL, secure.URL} {
|
||||
t.Run(upstream, func(t *testing.T) {
|
||||
selected, err := url.Parse(upstream)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
conn, err := dialTunnel(ctx, "example.com:443", selected)
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("unsafe upstream unexpectedly accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutesAndAuthentication(t *testing.T) {
|
||||
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Proxy-Authorization") != "" {
|
||||
|
||||
Vendored
+2
@@ -18,6 +18,8 @@ Local native changes are limited to:
|
||||
setting and attach the profile before navigation.
|
||||
- `MyCookieManager`: route cookie operations to the requested profile, capturing
|
||||
the store per operation so asynchronous calls cannot cross profiles.
|
||||
- macOS `FlutterWebViewController`: weakly register profile views so uninstall
|
||||
can release their WKWebViews even when Flutter retains a platform-view wrapper.
|
||||
|
||||
`lib/app/rpc/webview_profile.dart` uses the existing plugin method channel and
|
||||
serializes these internal settings. No extension JavaScript API is added.
|
||||
|
||||
+18
@@ -7,8 +7,25 @@ import FlutterMacOS
|
||||
final class GopeedProfiles {
|
||||
private static var proxyURLs: [String: String] = [:]
|
||||
private static var stores: [String: WKWebsiteDataStore] = [:]
|
||||
private static var views: [String: NSHashTable<FlutterWebViewController>] = [:]
|
||||
static func store(_ identifier: String) -> WKWebsiteDataStore? { stores[identifier] }
|
||||
|
||||
static func register(_ view: FlutterWebViewController, identifier: String) {
|
||||
let profileViews = views[identifier] ?? NSHashTable<FlutterWebViewController>.weakObjects()
|
||||
profileViews.add(view)
|
||||
views[identifier] = profileViews
|
||||
}
|
||||
|
||||
private static func disposeViews(_ identifier: String) {
|
||||
// Flutter may retain a removed platform-view wrapper beyond the frame
|
||||
// boundary. Release its WKWebView now, before deleting the named store.
|
||||
// The host has already closed all pages belonging to this profile.
|
||||
let profileViews = views.removeValue(forKey: identifier)
|
||||
for view in profileViews?.allObjects ?? [] {
|
||||
view.dispose(removeFromSuperview: true)
|
||||
}
|
||||
}
|
||||
|
||||
static func prepare(arguments: NSDictionary?, result: @escaping FlutterResult) {
|
||||
guard #available(macOS 14.0, iOS 17.0, *),
|
||||
let identifier = arguments?["gopeedProfileId"] as? String,
|
||||
@@ -45,6 +62,7 @@ final class GopeedProfiles {
|
||||
}
|
||||
// Deleting a named store alone can leave in-process cookie caches.
|
||||
// Clear every website data type before releasing/removing the store.
|
||||
disposeViews(identifier)
|
||||
let store = stores[identifier] ?? WKWebsiteDataStore(forIdentifier: uuid)
|
||||
store.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: Date.distantPast) {
|
||||
stores.removeValue(forKey: identifier)
|
||||
|
||||
+3
@@ -68,6 +68,9 @@ public class FlutterWebViewController: NSView, Disposable {
|
||||
webView!.settings = settings
|
||||
webView!.prepare()
|
||||
webView!.windowCreated = true
|
||||
if let identifier = initialSettings["gopeedProfileId"] as? String {
|
||||
GopeedProfiles.register(self, identifier: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder nsCoder: NSCoder) {
|
||||
|
||||
Reference in new issue
Block a user