@@ -4,11 +4,43 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "net/http"
8+ "strings"
79 "sync"
810
911 "github.com/nnemirovsky/iwdp-mcp/internal/webkit"
1012)
1113
14+ // httpStatusText returns the standard status text for an HTTP status code.
15+ func httpStatusText (code int ) string {
16+ text := http .StatusText (code )
17+ if text == "" {
18+ return "Unknown"
19+ }
20+ return text
21+ }
22+
23+ // mimeTypeFromHeaders extracts the MIME type from a Content-Type header, defaulting to text/plain.
24+ func mimeTypeFromHeaders (headers map [string ]string ) string {
25+ for k , v := range headers {
26+ if strings .EqualFold (k , "content-type" ) {
27+ // Strip charset etc: "text/html; charset=utf-8" → "text/html"
28+ if idx := strings .IndexByte (v , ';' ); idx >= 0 {
29+ return strings .TrimSpace (v [:idx ])
30+ }
31+ return v
32+ }
33+ }
34+ return "text/plain"
35+ }
36+
37+ // isAlreadyEnabledErr checks if the error is "Interception already enabled/disabled",
38+ // which happens when a previous session left state on WebKit's side.
39+ func isAlreadyEnabledErr (err error ) bool {
40+ return err != nil && (strings .Contains (err .Error (), "already enabled" ) ||
41+ strings .Contains (err .Error (), "already disabled" ))
42+ }
43+
1244// NetworkMonitor collects network requests and responses.
1345type NetworkMonitor struct {
1446 mu sync.Mutex
@@ -164,6 +196,7 @@ func SetExtraHeaders(ctx context.Context, client *webkit.Client, headers map[str
164196// InterceptedRequest holds an intercepted request waiting for a continue/response decision.
165197type InterceptedRequest struct {
166198 RequestID string `json:"request_id"`
199+ Stage string `json:"stage"`
167200 Request webkit.NetworkRequest `json:"request"`
168201}
169202
@@ -181,8 +214,11 @@ func NewInterceptionCollector() *InterceptionCollector {
181214 }
182215}
183216
184- // Start enables request interception and registers the event handler.
185- func (ic * InterceptionCollector ) Start (ctx context.Context , client * webkit.Client ) error {
217+ // Start enables request interception, adds an interception rule, and registers the event handler.
218+ // urlPattern is a URL pattern to intercept (empty string = all requests).
219+ // stage is "request" or "response" (empty defaults to "request").
220+ // isRegex controls whether urlPattern is treated as a regex.
221+ func (ic * InterceptionCollector ) Start (ctx context.Context , client * webkit.Client , urlPattern , stage string , isRegex bool ) error {
186222 ic .mu .Lock ()
187223 if ic .started {
188224 ic .mu .Unlock ()
@@ -191,14 +227,42 @@ func (ic *InterceptionCollector) Start(ctx context.Context, client *webkit.Clien
191227 ic .started = true
192228 ic .mu .Unlock ()
193229
230+ // Network domain must be enabled for interception events to be dispatched.
231+ _ , _ = client .Send (ctx , "Network.enable" , nil )
232+
194233 _ , err := client .Send (ctx , "Network.setInterceptionEnabled" , map [string ]interface {}{
195234 "enabled" : true ,
196235 })
197236 if err != nil {
237+ // "Interception already enabled" is non-fatal — a previous session may have
238+ // left it on. We still need to add rules and register the event handler.
239+ if ! isAlreadyEnabledErr (err ) {
240+ ic .mu .Lock ()
241+ ic .started = false
242+ ic .mu .Unlock ()
243+ return err
244+ }
245+ }
246+
247+ if stage == "" {
248+ stage = "request"
249+ }
250+
251+ // Register an interception rule — without this, no requestIntercepted events fire.
252+ _ , err = client .Send (ctx , "Network.addInterception" , map [string ]interface {}{
253+ "url" : urlPattern ,
254+ "stage" : stage ,
255+ "isRegex" : isRegex ,
256+ })
257+ if err != nil {
258+ // Roll back — disable interception if we can't add a rule.
259+ _ , _ = client .Send (ctx , "Network.setInterceptionEnabled" , map [string ]interface {}{
260+ "enabled" : false ,
261+ })
198262 ic .mu .Lock ()
199263 ic .started = false
200264 ic .mu .Unlock ()
201- return err
265+ return fmt . Errorf ( "adding interception rule: %w" , err )
202266 }
203267
204268 client .OnEvent ("Network.requestIntercepted" , func (method string , params json.RawMessage ) {
@@ -212,6 +276,24 @@ func (ic *InterceptionCollector) Start(ctx context.Context, client *webkit.Clien
212276 ic .mu .Lock ()
213277 ic .pending [evt .RequestID ] = & InterceptedRequest {
214278 RequestID : evt .RequestID ,
279+ Stage : "request" ,
280+ Request : evt .Request ,
281+ }
282+ ic .mu .Unlock ()
283+ })
284+
285+ client .OnEvent ("Network.responseIntercepted" , func (method string , params json.RawMessage ) {
286+ var evt struct {
287+ RequestID string `json:"requestId"`
288+ Request webkit.NetworkRequest `json:"request"`
289+ }
290+ if err := json .Unmarshal (params , & evt ); err != nil {
291+ return
292+ }
293+ ic .mu .Lock ()
294+ ic .pending [evt .RequestID ] = & InterceptedRequest {
295+ RequestID : evt .RequestID ,
296+ Stage : "response" ,
215297 Request : evt .Request ,
216298 }
217299 ic .mu .Unlock ()
@@ -220,12 +302,17 @@ func (ic *InterceptionCollector) Start(ctx context.Context, client *webkit.Clien
220302 return nil
221303}
222304
223- // Stop disables request interception.
305+ // Stop disables request interception and removes all interception rules .
224306func (ic * InterceptionCollector ) Stop (ctx context.Context , client * webkit.Client ) error {
225307 ic .mu .Lock ()
226308 ic .started = false
227309 ic .pending = make (map [string ]* InterceptedRequest )
228310 ic .mu .Unlock ()
311+ // removeInterception is best-effort — the disable call below will clear everything anyway.
312+ _ , _ = client .Send (ctx , "Network.removeInterception" , map [string ]interface {}{
313+ "url" : "" ,
314+ "stage" : "request" ,
315+ })
229316 _ , err := client .Send (ctx , "Network.setInterceptionEnabled" , map [string ]interface {}{
230317 "enabled" : false ,
231318 })
@@ -260,20 +347,50 @@ func SetRequestInterception(ctx context.Context, client *webkit.Client, enabled
260347}
261348
262349// InterceptContinue continues an intercepted request without modification.
263- func InterceptContinue (ctx context.Context , client * webkit.Client , requestID string ) error {
350+ func InterceptContinue (ctx context.Context , client * webkit.Client , requestID , stage string ) error {
351+ if stage == "" {
352+ stage = "request"
353+ }
264354 _ , err := client .Send (ctx , "Network.interceptContinue" , map [string ]string {
265355 "requestId" : requestID ,
356+ "stage" : stage ,
266357 })
267358 return err
268359}
269360
270361// InterceptWithResponse provides a custom response for an intercepted request.
271- func InterceptWithResponse (ctx context.Context , client * webkit.Client , requestID string , statusCode int , headers map [string ]string , body string ) error {
362+ // For request-stage interceptions, uses Network.interceptRequestWithResponse (synthetic response).
363+ // For response-stage interceptions, uses Network.interceptWithResponse (modify received response).
364+ func InterceptWithResponse (ctx context.Context , client * webkit.Client , requestID string , stage string , statusCode int , headers map [string ]string , content string , base64Encoded bool ) error {
365+ if stage == "" {
366+ stage = "request"
367+ }
368+ if headers == nil {
369+ headers = map [string ]string {}
370+ }
371+
372+ if stage == "request" {
373+ // interceptRequestWithResponse: synthetic response at request stage (skip network).
374+ _ , err := client .Send (ctx , "Network.interceptRequestWithResponse" , map [string ]interface {}{
375+ "requestId" : requestID ,
376+ "status" : statusCode ,
377+ "statusText" : httpStatusText (statusCode ),
378+ "mimeType" : mimeTypeFromHeaders (headers ),
379+ "content" : content ,
380+ "base64Encoded" : base64Encoded ,
381+ "headers" : headers ,
382+ })
383+ return err
384+ }
385+
386+ // interceptWithResponse: modify response at response stage.
272387 _ , err := client .Send (ctx , "Network.interceptWithResponse" , map [string ]interface {}{
273- "requestId" : requestID ,
274- "statusCode" : statusCode ,
275- "headers" : headers ,
276- "body" : body ,
388+ "requestId" : requestID ,
389+ "stage" : stage ,
390+ "statusCode" : statusCode ,
391+ "headers" : headers ,
392+ "content" : content ,
393+ "base64Encoded" : base64Encoded ,
277394 })
278395 return err
279396}
0 commit comments