mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-07 17:10:29 +00:00
Isolate extension WebView profiles and follow host proxy settings
This commit is contained in:
1 parent
cee5f3336e
commit
a0e84a75b1
556 files changed
+71097
-31
No files matched your search
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/logger"
|
||||
webviewproxy "github.com/GopeedLab/gopeed/internal/webview/proxy"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
@@ -97,8 +98,10 @@ type Progress struct {
|
||||
}
|
||||
|
||||
type Downloader struct {
|
||||
Logger *logger.Logger
|
||||
ExtensionLogger *logger.Logger
|
||||
webviewProxyLock sync.Mutex
|
||||
webviewProxy *webviewproxy.Server
|
||||
Logger *logger.Logger
|
||||
ExtensionLogger *logger.Logger
|
||||
|
||||
cfg *DownloaderConfig
|
||||
fetcherCache map[string]fetcher.Fetcher
|
||||
@@ -1114,6 +1117,11 @@ func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
||||
|
||||
func (d *Downloader) Close() error {
|
||||
d.closed.Store(true)
|
||||
d.webviewProxyLock.Lock()
|
||||
if d.webviewProxy != nil {
|
||||
_ = d.webviewProxy.Close()
|
||||
}
|
||||
d.webviewProxyLock.Unlock()
|
||||
|
||||
closeArr := []func() error{
|
||||
d.pauseAll,
|
||||
|
||||
@@ -68,6 +68,9 @@ type IsAvailableResult struct {
|
||||
}
|
||||
|
||||
type PageOpenParams struct {
|
||||
ProfileID string `json:"profileId,omitempty"`
|
||||
DataPath string `json:"dataPath,omitempty"`
|
||||
ProxyURL string `json:"proxyUrl,omitempty"`
|
||||
Headless bool `json:"headless,omitempty"`
|
||||
Debug bool `json:"debug,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
@@ -78,6 +81,9 @@ type PageOpenParams struct {
|
||||
|
||||
func NewPageOpenParams(opts OpenOptions) PageOpenParams {
|
||||
return PageOpenParams{
|
||||
ProfileID: opts.ProfileID,
|
||||
DataPath: opts.DataPath,
|
||||
ProxyURL: opts.ProxyURL,
|
||||
Headless: opts.Headless,
|
||||
Debug: opts.Debug,
|
||||
Title: opts.Title,
|
||||
|
||||
@@ -29,6 +29,10 @@ type Page interface {
|
||||
}
|
||||
|
||||
type OpenOptions struct {
|
||||
// Host-owned fields are never parsed from extension JavaScript options.
|
||||
ProfileID string
|
||||
DataPath string
|
||||
ProxyURL string
|
||||
Headless bool
|
||||
Debug bool
|
||||
Title string
|
||||
|
||||
@@ -336,3 +336,15 @@ func (p *fakePage) Close() error {
|
||||
p.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestExtensionCannotSupplyHostProfileOrProxy(t *testing.T) {
|
||||
opener := &fakeOpener{page: &fakePage{}}
|
||||
runtime := NewRuntime(opener, true)
|
||||
_, err := runtime.Open(map[string]any{"profileId": "other-extension", "dataPath": "/tmp/other-profile", "proxyUrl": "http://example.com:1234", "ProfileID": "other"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opener.opts.ProfileID != "" || opener.opts.DataPath != "" || opener.opts.ProxyURL != "" {
|
||||
t.Fatalf("extension controlled host settings: %+v", opener.opts)
|
||||
}
|
||||
}
|
||||
@@ -365,7 +365,7 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
|
||||
engine, session := d.newExtensionEngine()
|
||||
defer session.CloseIfIdle()
|
||||
gopeed.Runtime = &InstanceRuntime{
|
||||
WebView: d.newExtensionWebViewRuntime(session),
|
||||
WebView: d.newExtensionWebViewRuntime(session, ext.buildIdentity()),
|
||||
}
|
||||
err = injectGopeed(engine.Runtime, gopeed)
|
||||
if err != nil {
|
||||
|
||||
@@ -30,7 +30,7 @@ func (d *Downloader) NewExtensionEngine(ext *Extension, settings map[string]any)
|
||||
identity: ext.buildIdentity(),
|
||||
},
|
||||
Runtime: &InstanceRuntime{
|
||||
WebView: d.newExtensionWebViewRuntime(session),
|
||||
WebView: d.newExtensionWebViewRuntime(session, ext.buildIdentity()),
|
||||
},
|
||||
}
|
||||
if err := injectGopeed(engine.Runtime, gopeed); err != nil {
|
||||
|
||||
@@ -127,13 +127,13 @@ func (d *Downloader) newExtensionEngine() (*engine.Engine, *engineSession) {
|
||||
return e, session
|
||||
}
|
||||
|
||||
func (d *Downloader) newExtensionWebViewRuntime(session *engineSession) *enginewebview.Runtime {
|
||||
func (d *Downloader) newExtensionWebViewRuntime(session *engineSession, identity string) *enginewebview.Runtime {
|
||||
var (
|
||||
opener enginewebview.Opener
|
||||
available bool
|
||||
)
|
||||
if provider := d.cfg.WebViewProvider; provider != nil && provider.IsAvailable() {
|
||||
opener = provider
|
||||
opener = &extensionWebViewOpener{downloader: d, provider: provider, identity: identity}
|
||||
available = true
|
||||
}
|
||||
runtime := enginewebview.NewRuntime(opener, available)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
webviewproxy "github.com/GopeedLab/gopeed/internal/webview/proxy"
|
||||
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
|
||||
)
|
||||
|
||||
// extensionWebViewOpener binds browser state to the owning extension. Neither
|
||||
// identity, filesystem paths nor proxy settings are accepted from JavaScript.
|
||||
type extensionWebViewOpener struct {
|
||||
downloader *Downloader
|
||||
provider enginewebview.Provider
|
||||
identity string
|
||||
}
|
||||
|
||||
func (o *extensionWebViewOpener) Open(opts enginewebview.OpenOptions) (enginewebview.Page, error) {
|
||||
if o.identity == "" {
|
||||
return nil, fmt.Errorf("webview extension identity is required")
|
||||
}
|
||||
sum := sha256.Sum256([]byte(o.identity))
|
||||
opts.ProfileID = fmt.Sprintf("%x-%x-%x-%x-%x", sum[:4], sum[4:6], sum[6:8], sum[8:10], sum[10:16])
|
||||
path, err := filepath.Abs(filepath.Join(o.downloader.cfg.StorageDir, "webview", fmt.Sprintf("%x", sum[:])))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.DataPath = path
|
||||
opts.ProxyURL, err = o.downloader.webviewProxyURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return o.provider.Open(opts)
|
||||
}
|
||||
|
||||
func (d *Downloader) webviewProxyURL() (string, error) {
|
||||
d.webviewProxyLock.Lock()
|
||||
defer d.webviewProxyLock.Unlock()
|
||||
if d.closed.Load() {
|
||||
return "", fmt.Errorf("downloader is closed")
|
||||
}
|
||||
if d.webviewProxy == nil {
|
||||
server, err := webviewproxy.New(func(r *http.Request) (*url.URL, error) {
|
||||
if d.blob != nil && d.blob.IsURL(r.URL.String()) {
|
||||
return nil, nil
|
||||
}
|
||||
d.configLock.RLock()
|
||||
var cfg base.DownloaderProxyConfig
|
||||
if d.cfg.Proxy != nil {
|
||||
cfg = *d.cfg.Proxy
|
||||
}
|
||||
d.configLock.RUnlock()
|
||||
handler := cfg.ToHandler()
|
||||
if handler == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return handler(r)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
d.webviewProxy = server
|
||||
}
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
return d.webviewProxy.SOCKSURL(), nil
|
||||
}
|
||||
return d.webviewProxy.URL(), nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
|
||||
)
|
||||
|
||||
type profileCaptureProvider struct{ options []enginewebview.OpenOptions }
|
||||
|
||||
func (*profileCaptureProvider) IsAvailable() bool { return true }
|
||||
func (p *profileCaptureProvider) Open(opts enginewebview.OpenOptions) (enginewebview.Page, error) {
|
||||
p.options = append(p.options, opts)
|
||||
return fakeRuntimeWebViewPage{}, nil
|
||||
}
|
||||
|
||||
func TestWebViewProfileBoundToExtension(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
d := &Downloader{cfg: &DownloaderConfig{StorageDir: root, DownloaderStoreConfig: &base.DownloaderStoreConfig{}}, configLock: &sync.RWMutex{}}
|
||||
t.Cleanup(func() {
|
||||
if d.webviewProxy != nil {
|
||||
d.webviewProxy.Close()
|
||||
}
|
||||
})
|
||||
provider := &profileCaptureProvider{}
|
||||
for _, id := range []string{"extension-a", "extension-b", "extension-a", "../../escape"} {
|
||||
opener := &extensionWebViewOpener{downloader: d, provider: provider, identity: id}
|
||||
_, err := opener.Open(enginewebview.OpenOptions{ProfileID: "forged", DataPath: "/tmp/forged", ProxyURL: "http://wrong:80"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
a, b, reopen := provider.options[0], provider.options[1], provider.options[2]
|
||||
if a.ProfileID == b.ProfileID || a.DataPath == b.DataPath {
|
||||
t.Fatal("different extensions shared browser storage")
|
||||
}
|
||||
if a.ProfileID != reopen.ProfileID || a.DataPath != reopen.DataPath {
|
||||
t.Fatal("same extension lost profile identity")
|
||||
}
|
||||
for _, opts := range provider.options {
|
||||
if filepath.Dir(opts.DataPath) != filepath.Join(root, "webview") {
|
||||
t.Fatalf("profile escaped storage root: %s", opts.DataPath)
|
||||
}
|
||||
if opts.ProxyURL != d.webviewProxy.URL() && opts.ProxyURL != d.webviewProxy.SOCKSURL() {
|
||||
t.Fatal("host proxy was overridden")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user