Skip to content
This repository was archived by the owner on Jan 30, 2025. It is now read-only.

Commit 112193c

Browse files
author
Ivan Mirić
authored
Merge pull request #204 from grafana/feat/105-k6-blockhostnames
Add request blocking based on k6 `blockHostnames` option
2 parents ed0de4f + 49b6724 commit 112193c

9 files changed

+300
-78
lines changed

common/browser.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,11 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) {
230230
return
231231
}
232232

233+
session := b.connSessions.getSession(ev.SessionID)
234+
233235
switch evti.Type {
234236
case "background_page":
235-
p, err := NewPage(b.ctx, b.connSessions.getSession(ev.SessionID), browserCtx, evti.TargetID, nil, false, b.logger)
237+
p, err := NewPage(b.ctx, session, browserCtx, evti.TargetID, nil, false, b.logger)
236238
if err != nil {
237239
isRunning := atomic.LoadInt64(&b.state) == BrowserStateOpen && b.IsConnected() //b.conn.isConnected()
238240
if _, ok := err.(*websocket.CloseError); !ok && !isRunning {
@@ -272,7 +274,7 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) {
272274

273275
b.logger.Debugf("Browser:onAttachedToTarget:page", "sid:%v tid:%v opener nil:%t", ev.SessionID, evti.TargetID, opener == nil)
274276

275-
p, err := NewPage(b.ctx, b.connSessions.getSession(ev.SessionID), browserCtx, evti.TargetID, opener, true, b.logger)
277+
p, err := NewPage(b.ctx, session, browserCtx, evti.TargetID, opener, true, b.logger)
276278
if err != nil {
277279
isRunning := atomic.LoadInt64(&b.state) == BrowserStateOpen && b.IsConnected() //b.conn.isConnected()
278280
if _, ok := err.(*websocket.CloseError); !ok && !isRunning {

common/browser_context.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func (b *BrowserContext) ExposeBinding(name string, callback goja.Callable, opts
157157
}
158158

159159
func (b *BrowserContext) ExposeFunction(name string, callback goja.Callable) {
160-
k6Throw(b.ctx, "BrowserContext.newCDPSession(name, callback) has not been implemented yet")
160+
k6Throw(b.ctx, "BrowserContext.exposeFunction(name, callback) has not been implemented yet")
161161
}
162162

163163
// GrantPermissions enables the specified permissions, all others will be disabled.
@@ -245,7 +245,7 @@ func (b *BrowserContext) Pages() []api.Page {
245245
}
246246

247247
func (b *BrowserContext) Route(url goja.Value, handler goja.Callable) {
248-
k6Throw(b.ctx, "BrowserContext.setHTTPCredentials(httpCredentials) has not been implemented yet")
248+
k6Throw(b.ctx, "BrowserContext.route(url, handler) has not been implemented yet")
249249
}
250250

251251
// SetDefaultNavigationTimeout sets the default navigation timeout in milliseconds.
@@ -263,7 +263,7 @@ func (b *BrowserContext) SetDefaultTimeout(timeout int64) {
263263
}
264264

265265
func (b *BrowserContext) SetExtraHTTPHeaders(headers map[string]string) {
266-
k6Throw(b.ctx, "BrowserContext.setHTTPCredentials(httpCredentials) has not been implemented yet")
266+
k6Throw(b.ctx, "BrowserContext.setExtraHTTPHeaders(headers) has not been implemented yet")
267267
}
268268

269269
// SetGeolocation overrides the geo location of the user.

common/frame_manager.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"context"
2525
"errors"
2626
"fmt"
27+
"strings"
2728
"sync"
2829
"sync/atomic"
2930
"time"
@@ -573,7 +574,8 @@ func (m *FrameManager) NavigateFrame(frame *Frame, url string, opts goja.Value)
573574
"fmid:%d fid:%v furl:%s url:%s", fmid, fid, furl, url)
574575

575576
rt := k6common.GetRuntime(m.ctx)
576-
defaultReferer := m.page.mainFrameSession.getNetworkManager().extraHTTPHeaders["referer"]
577+
netMgr := m.page.mainFrameSession.getNetworkManager()
578+
defaultReferer := netMgr.extraHTTPHeaders["referer"]
577579
parsedOpts := NewFrameGotoOptions(defaultReferer, time.Duration(m.timeoutSettings.navigationTimeout())*time.Second)
578580
if err := parsedOpts.Parse(m.ctx, opts); err != nil {
579581
k6common.Throw(rt, fmt.Errorf("failed parsing options: %w", err))
@@ -627,10 +629,16 @@ func (m *FrameManager) NavigateFrame(frame *Frame, url string, opts goja.Value)
627629
if err != nil {
628630
k6common.Throw(rt, err)
629631
}
632+
630633
event = data.(*NavigationEvent)
631634
if event.newDocument.documentID != newDocumentID {
632-
k6common.Throw(rt, errors.New("navigation interrupted by another one"))
633-
} else if event.err != nil {
635+
m.logger.Debugf("FrameManager:NavigateFrame:interrupted",
636+
"fmid:%d fid:%v furl:%s url:%s docID:%s newDocID:%s",
637+
fmid, fid, furl, url, event.newDocument.documentID, newDocumentID)
638+
} else if event.err != nil &&
639+
// TODO: A more graceful way of avoiding Throw()?
640+
!(netMgr.userReqInterceptionEnabled &&
641+
strings.Contains(event.err.Error(), "ERR_BLOCKED_BY_CLIENT")) {
634642
k6common.Throw(rt, event.err)
635643
}
636644
} else {

common/frame_session.go

+23-26
Original file line numberDiff line numberDiff line change
@@ -112,26 +112,15 @@ func NewFrameSession(
112112
},
113113
}
114114

115+
var parentNM *NetworkManager
115116
if fs.parent != nil {
116-
fs.networkManager, err = NewNetworkManager(ctx, session, fs.manager, fs.parent.networkManager)
117-
if err != nil {
118-
logger.Debugf(
119-
"NewFrameSession:NewNetworkManager",
120-
"sid:%v tid:%v err:%v",
121-
session.id, targetID, err)
122-
123-
return nil, err
124-
}
125-
} else {
126-
fs.networkManager, err = NewNetworkManager(ctx, session, fs.manager, nil)
127-
if err != nil {
128-
logger.Debugf(
129-
"NewFrameSession:NewNetworkManager#2",
130-
"sid:%v tid:%v err:%v",
131-
session.id, targetID, err)
132-
133-
return nil, err
134-
}
117+
parentNM = fs.parent.networkManager
118+
}
119+
fs.networkManager, err = NewNetworkManager(ctx, session, fs.manager, parentNM)
120+
if err != nil {
121+
logger.Debugf("NewFrameSession:NewNetworkManager", "sid:%v tid:%v err:%v",
122+
session.id, targetID, err)
123+
return nil, err
135124
}
136125

137126
action := browser.GetWindowForTarget().WithTargetID(fs.targetID)
@@ -372,8 +361,11 @@ func (fs *FrameSession) initOptions() error {
372361
fs.logger.Debugf("NewFrameSession:initOptions",
373362
"sid:%v tid:%v", fs.session.id, fs.targetID)
374363

375-
opts := fs.manager.page.browserCtx.opts
376-
optActions := []Action{}
364+
var (
365+
opts = fs.manager.page.browserCtx.opts
366+
optActions = []Action{}
367+
state = k6lib.GetState(fs.ctx)
368+
)
377369

378370
if fs.isMainFrame() {
379371
optActions = append(optActions, emulation.SetFocusEmulationEnabled(true))
@@ -413,9 +405,15 @@ func (fs *FrameSession) initOptions() error {
413405
return err
414406
}
415407
fs.updateExtraHTTPHeaders(true)
416-
if err := fs.updateRequestInterception(true); err != nil {
408+
409+
var reqIntercept bool
410+
if state.Options.BlockedHostnames.Trie != nil {
411+
reqIntercept = true
412+
}
413+
if err := fs.updateRequestInterception(reqIntercept); err != nil {
417414
return err
418415
}
416+
419417
fs.updateOffline(true)
420418
fs.updateHttpCredentials(true)
421419
if err := fs.updateEmulateMedia(true); err != nil {
@@ -990,10 +988,9 @@ func (fs *FrameSession) updateOffline(initial bool) {
990988
}
991989
}
992990

993-
func (fs *FrameSession) updateRequestInterception(initial bool) error {
994-
fs.logger.Debugf("NewFrameSession:updateRequestInterception", "sid:%v tid:%v", fs.session.id, fs.targetID)
995-
996-
return fs.networkManager.setRequestInterception(fs.page.hasRoutes())
991+
func (fs *FrameSession) updateRequestInterception(enable bool) error {
992+
fs.logger.Debugf("NewFrameSession:updateRequestInterception", "sid:%v tid:%v on:%v", fs.session.id, fs.targetID, enable)
993+
return fs.networkManager.setRequestInterception(enable || fs.page.hasRoutes())
997994
}
998995

999996
func (fs *FrameSession) updateViewport() error {

common/network_manager.go

+67-26
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"context"
2525
"fmt"
2626
"net"
27+
"net/url"
2728
"strconv"
2829
"sync"
2930
"time"
@@ -36,6 +37,7 @@ import (
3637
"github.com/dop251/goja"
3738
k6common "go.k6.io/k6/js/common"
3839
k6lib "go.k6.io/k6/lib"
40+
k6types "go.k6.io/k6/lib/types"
3941
k6stats "go.k6.io/k6/stats"
4042
)
4143

@@ -48,7 +50,7 @@ type NetworkManager struct {
4850

4951
ctx context.Context
5052
logger *Logger
51-
session *Session
53+
session session
5254
parent *NetworkManager
5355
frameManager *FrameManager
5456
credentials *Credentials
@@ -235,10 +237,20 @@ func (m *NetworkManager) handleRequestRedirect(req *Request, redirectResponse *n
235237
}
236238

237239
func (m *NetworkManager) initDomains() error {
238-
action := network.Enable()
239-
if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil {
240-
return fmt.Errorf("unable to execute %T: %w", action, err)
240+
actions := []Action{network.Enable()}
241+
242+
// Only enable the Fetch domain if necessary, as it has a performance overhead.
243+
if m.userReqInterceptionEnabled {
244+
actions = append(actions,
245+
network.SetCacheDisabled(true),
246+
fetch.Enable().WithPatterns([]*fetch.RequestPattern{{URLPattern: "*"}}))
241247
}
248+
for _, action := range actions {
249+
if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil {
250+
return fmt.Errorf("unable to execute %T: %w", action, err)
251+
}
252+
}
253+
242254
return nil
243255
}
244256

@@ -250,6 +262,7 @@ func (m *NetworkManager) initEvents() {
250262
cdproto.EventNetworkRequestWillBeSent,
251263
cdproto.EventNetworkRequestServedFromCache,
252264
cdproto.EventNetworkResponseReceived,
265+
cdproto.EventFetchRequestPaused,
253266
}, chHandler)
254267

255268
go func() {
@@ -279,6 +292,8 @@ func (m *NetworkManager) handleEvents(in <-chan Event) bool {
279292
m.onRequestServedFromCache(ev)
280293
case *network.EventResponseReceived:
281294
m.onResponseReceived(ev)
295+
case *fetch.EventRequestPaused:
296+
m.onRequestPaused(ev)
282297
}
283298
}
284299
return true
@@ -360,32 +375,58 @@ func (m *NetworkManager) onRequest(event *network.EventRequestWillBeSent, interc
360375
m.reqsMu.Unlock()
361376
m.emitRequestMetrics(req)
362377
m.frameManager.requestStarted(req)
378+
}
363379

364-
if m.userReqInterceptionEnabled {
365-
state := k6lib.GetState(m.ctx)
366-
ip := net.ParseIP(req.url.Host)
367-
blockedHosts := state.Options.BlockedHostnames.Trie
368-
if blockedHosts != nil && ip == nil {
369-
if match, blocked := blockedHosts.Contains(req.url.Host); blocked {
370-
// Tell browser we've blocked this request.
371-
fetch.FailRequest(fetch.RequestID(req.getID()), network.ErrorReasonBlockedByClient)
372-
373-
// Throw exception into JS runtime
374-
rt := k6common.GetRuntime(m.ctx)
375-
// TODO: create PR to make netext.BlockedHostError a public struct in k6 perhaps?
376-
k6common.Throw(rt, fmt.Errorf("hostname (%s) is in a blocked pattern (%s)", req.url.Host, match))
377-
}
380+
func (m *NetworkManager) onRequestPaused(event *fetch.EventRequestPaused) {
381+
m.logger.Debugf("NetworkManager:onRequestPaused",
382+
"sid:%s url:%v", m.session.ID(), event.Request.URL)
383+
defer m.logger.Debugf("NetworkManager:onRequestPaused:return",
384+
"sid:%s url:%v", m.session.ID(), event.Request.URL)
385+
386+
var (
387+
failReason string
388+
state = k6lib.GetState(m.ctx)
389+
)
390+
391+
defer func() { m.failOrContinueRequest(event, failReason) }()
392+
393+
purl, err := url.Parse(event.Request.URL)
394+
if err != nil {
395+
m.logger.Errorf("NetworkManager:onRequestPaused",
396+
"error parsing URL: %s", err.Error())
397+
return
398+
}
399+
400+
failReason = handleBlockedHosts(purl, state.Options.BlockedHostnames.Trie)
401+
}
402+
403+
func (m *NetworkManager) failOrContinueRequest(event *fetch.EventRequestPaused, failReason string) {
404+
if failReason != "" {
405+
action := fetch.FailRequest(event.RequestID, network.ErrorReasonBlockedByClient)
406+
if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil {
407+
m.logger.Errorf("NetworkManager:onRequestPaused",
408+
"error interrupting request: %s", err.Error())
409+
} else {
410+
m.logger.Warnf("NetworkManager:onRequestPaused",
411+
"request %s %s was interrupted: %s", event.Request.Method, event.Request.URL, failReason)
412+
return
378413
}
414+
}
415+
action := fetch.ContinueRequest(event.RequestID)
416+
if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil {
417+
m.logger.Errorf("NetworkManager:onRequestPaused",
418+
"error continuing request: %s", err.Error())
419+
}
420+
}
379421

380-
/*
381-
TODO: is there a way to do IP filtering without requiring a lookup here?
382-
for _, ipnet := range state.Options.BlacklistIPs {
383-
if ipnet.Contains(ev.Request.URL) {
384-
return "", netext.BlackListedIPError{ip: remote.IP, net: ipnet}
385-
}
386-
}
387-
*/
422+
func handleBlockedHosts(u *url.URL, blockedHosts *k6types.HostnameTrie) string {
423+
ip := net.ParseIP(u.Host)
424+
if ip == nil {
425+
if match, blocked := blockedHosts.Contains(u.Host); blocked {
426+
return fmt.Sprintf("hostname %s is in a blocked pattern (%s)", u.Host, match)
427+
}
388428
}
429+
return ""
389430
}
390431

391432
func (m *NetworkManager) onRequestServedFromCache(event *network.EventRequestServedFromCache) {

0 commit comments

Comments
 (0)