Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,32 @@ func BearerAuth(token string) browse.HandlerFunc {
return HeaderAuth("Authorization", "Bearer "+token)
}

// HeaderAuth returns middleware that sets a custom header on all requests.
// HeaderAuth returns middleware that injects a custom header, scoped to the
// navigation's own origin. Unlike a session-wide Network.setExtraHTTPHeaders
// (which sends the header to every host the page contacts), this adds the header
// only to same-origin requests — so an auth token isn't leaked to cross-origin
// subresources or a redirect destination.
func HeaderAuth(name, value string) browse.HandlerFunc {
return func(c *browse.Context) {
page := c.Page()
if page == nil {
c.Next()
return
}
_, _ = page.Call("Network.enable", nil)
_, err := page.Call("Network.setExtraHTTPHeaders", map[string]any{
"headers": map[string]string{
name: value,
remove, err := page.InterceptRequests(browse.RequestRule{
Name: "auth-header:" + name,
Decide: func(r browse.InterceptedRequest) browse.RequestVerdict {
if r.SameOriginAsTop() {
return browse.RequestVerdict{AddHeaders: map[string]string{name: value}}
}
return browse.RequestVerdict{}
},
})
if err != nil {
c.AbortWithError(fmt.Errorf("browse: failed to set auth header: %w", err))
return
}
defer remove()
c.Next()
}
}
38 changes: 16 additions & 22 deletions middleware/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,32 @@ func BlockResources(resourceTypes ...string) browse.HandlerFunc {
return
}

// Enable Fetch domain to intercept requests
patterns := make([]map[string]string, 0, len(resourceTypes))
blocked := make(map[string]struct{}, len(resourceTypes))
for _, rt := range resourceTypes {
patterns = append(patterns, map[string]string{
"resourceType": rt,
})
blocked[rt] = struct{}{}
}

_, err := page.Call("Fetch.enable", map[string]any{
"patterns": patterns,
// Register a rule on the page's shared request interceptor so resource
// blocking coexists with the URL-policy guard and header injection
// instead of fighting over Fetch.enable.
remove, err := page.InterceptRequests(browse.RequestRule{
Name: "block-resources",
ResourceTypes: resourceTypes,
Decide: func(r browse.InterceptedRequest) browse.RequestVerdict {
if _, ok := blocked[r.ResourceType]; ok {
return browse.RequestVerdict{Block: true, BlockReason: "BlockedByClient"}
}
return browse.RequestVerdict{}
},
})
if err != nil {
// Fetch domain might not be available; proceed without blocking
// Fetch domain might not be available; proceed without blocking.
c.Next()
return
}

// Listen for intercepted requests and fail them
page.OnSession("Fetch.requestPaused", func(params map[string]any) {
requestID, _ := params["requestId"].(string)
if requestID != "" {
_, _ = page.Call("Fetch.failRequest", map[string]any{
"requestId": requestID,
"errorReason": "BlockedByClient",
})
}
})
defer remove()

c.Next()

// Disable Fetch after task completes
_, _ = page.Call("Fetch.disable", nil)
}
}

Expand Down
20 changes: 20 additions & 0 deletions page.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"strings"
"sync"
"time"

"go.klarlabs.de/scout/internal/cdp"
Expand All @@ -30,6 +31,10 @@ type Page struct {
rootNodeID int64 // cached DOM root node ID
urlValidator URLValidator
flattenedNodes []flatNode // cached flattened DOM (pierce: true)

fetch *fetchInterceptor // lazy CDP Fetch interception multiplexer
navOrigin string // origin of the last caller-initiated Navigate
navOriginMu sync.Mutex
}

// call sends a CDP command scoped to this page's session, with a per-call
Expand Down Expand Up @@ -77,6 +82,17 @@ func newPage(conn *cdp.Conn, targetID string, timeout time.Duration, validator U
}
}

// Re-validate every navigation/redirect against the URL policy at request
// time — this blocks a public URL that redirects to an internal host, which
// the one-shot pre-navigation check can't catch. Trusted (AllowPrivateIPs)
// sessions skip it.
if !p.urlValidator.AllowPrivateIPs {
if err := p.installURLPolicy(); err != nil {
cancel()
return nil, fmt.Errorf("browse: failed to install URL policy: %w", err)
}
}

return p, nil
}

Expand All @@ -87,6 +103,10 @@ func (p *Page) Navigate(rawURL string) error {
return &NavigationError{URL: rawURL, Err: err}
}

// Record the intended target origin so header-injection rules can scope to it
// (and not leak to cross-origin subresources or redirect destinations).
p.setTopLevelOrigin(originOf(rawURL))

// Invalidate cached root node ID and flattened DOM on navigation
p.rootNodeID = 0
p.flattenedNodes = nil
Expand Down
Loading
Loading