99
1010const default_rest_port = "65444" ;
1111const default_scrub_parameters = false ;
12+ const default_collecting = true ;
13+ const default_domain_include = "" ;
14+ const default_domain_exclude = "" ;
15+
16+ // Request timing map — stores start times keyed by requestId
17+ const requestTimings = new Map ( ) ;
18+
19+ // Initiator map — stores the origin that triggered each request
20+ const requestInitiators = new Map ( ) ;
21+
22+ // Connected viewer ports for live streaming
23+ const viewerPorts = new Set ( ) ;
24+
25+ function broadcastToViewers ( data ) {
26+ for ( const port of viewerPorts ) {
27+ try {
28+ port . postMessage ( data ) ;
29+ } catch ( e ) {
30+ viewerPorts . delete ( port ) ;
31+ }
32+ }
33+ }
1234
1335// hashing function courtesy of bryc https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
1436const cyrb53 = ( str , seed = 42 ) => {
@@ -23,22 +45,56 @@ const cyrb53 = (str, seed = 42) => {
2345 return 4294967296 * ( 2097151 & h2 ) + ( h1 >>> 0 ) ;
2446} ;
2547
26- function scrubber ( match , p1 , offset , string ) {
48+ function scrubber ( match , p1 , _offset , _string ) {
2749 return "?SCRUBBED_hash=" + cyrb53 ( p1 ) ;
2850}
2951
52+ function updateBadge ( collecting ) {
53+ if ( collecting ) {
54+ chrome . action . setBadgeText ( { text : "" } ) ;
55+ } else {
56+ chrome . action . setBadgeText ( { text : "OFF" } ) ;
57+ chrome . action . setBadgeBackgroundColor ( { color : "#cc0000" } ) ;
58+ }
59+ }
60+
61+ function domainMatches ( hostname , domainList ) {
62+ return domainList . some ( domain => hostname === domain || hostname . endsWith ( "." + domain ) ) ;
63+ }
64+
3065async function logResponse ( details ) {
3166 // Get settings from storage every time, as the service worker can be terminated.
3267 const items = await chrome . storage . local . get ( {
3368 rest_port : default_rest_port ,
34- scrub_parameters : default_scrub_parameters
69+ scrub_parameters : default_scrub_parameters ,
70+ collecting : default_collecting ,
71+ domain_include : default_domain_include ,
72+ domain_exclude : default_domain_exclude
3573 } ) ;
3674
75+ // If collection is paused, do nothing
76+ if ( ! items . collecting ) return ;
77+
3778 const url_backend = `http://127.0.0.1:${ items . rest_port } /add_record` ;
3879
3980 // Avoid feedback loop and internal browser requests
4081 if ( details . url . startsWith ( url_backend ) || details . tabId < 0 ) return ;
4182
83+ // Domain filtering
84+ try {
85+ const hostname = new URL ( details . url ) . hostname ;
86+ const includeList = items . domain_include . split ( "\n" ) . map ( s => s . trim ( ) . toLowerCase ( ) ) . filter ( Boolean ) ;
87+ const excludeList = items . domain_exclude . split ( "\n" ) . map ( s => s . trim ( ) . toLowerCase ( ) ) . filter ( Boolean ) ;
88+
89+ if ( includeList . length > 0 ) {
90+ if ( ! domainMatches ( hostname . toLowerCase ( ) , includeList ) ) return ;
91+ } else if ( excludeList . length > 0 ) {
92+ if ( domainMatches ( hostname . toLowerCase ( ) , excludeList ) ) return ;
93+ }
94+ } catch ( _e ) {
95+ // If URL parsing fails, proceed anyway
96+ }
97+
4298 const headers = details . responseHeaders ;
4399 let data = {
44100 url : details . url ,
@@ -49,6 +105,20 @@ async function logResponse(details) {
49105 type : details . type
50106 } ;
51107
108+ // Compute request duration if we have a start time
109+ const startTime = requestTimings . get ( details . requestId ) ;
110+ if ( startTime !== undefined ) {
111+ data . duration_ms = Math . round ( details . timeStamp - startTime ) ;
112+ requestTimings . delete ( details . requestId ) ;
113+ }
114+
115+ // Attach initiator origin if captured from onBeforeRequest
116+ const initiator = requestInitiators . get ( details . requestId ) ;
117+ if ( initiator ) {
118+ data . initiator = initiator ;
119+ requestInitiators . delete ( details . requestId ) ;
120+ }
121+
52122 for ( const header of headers ) {
53123 const headerName = header . name . toLowerCase ( ) ;
54124 if ( headerName === 'content-length' ) {
@@ -90,17 +160,115 @@ async function logResponse(details) {
90160 } catch ( error ) {
91161 console . error ( "HTTP Graph Error: Could not send data to backend." , error ) ;
92162 }
163+
164+ broadcastToViewers ( finalData ) ;
93165}
94166
167+ const requestFilter = { urls : [ "http://*/*" , "https://*/*" ] } ;
168+
169+ chrome . webRequest . onBeforeRequest . addListener (
170+ ( details ) => {
171+ requestTimings . set ( details . requestId , details . timeStamp ) ;
172+ if ( details . initiator && details . initiator !== "null" ) {
173+ requestInitiators . set ( details . requestId , details . initiator ) ;
174+ }
175+ } ,
176+ requestFilter
177+ ) ;
178+
95179chrome . webRequest . onCompleted . addListener (
96180 logResponse ,
97- { urls : [ "http://*/*" , "https://*/*" ] } ,
181+ requestFilter ,
98182 [ "responseHeaders" ]
99183) ;
100184
185+ chrome . webRequest . onBeforeRedirect . addListener (
186+ async ( details ) => {
187+ const items = await chrome . storage . local . get ( {
188+ rest_port : default_rest_port ,
189+ collecting : default_collecting ,
190+ domain_include : default_domain_include ,
191+ domain_exclude : default_domain_exclude
192+ } ) ;
193+
194+ if ( ! items . collecting ) return ;
195+
196+ const url_backend = `http://127.0.0.1:${ items . rest_port } /add_record` ;
197+ if ( details . url . startsWith ( url_backend ) || details . tabId < 0 ) return ;
198+
199+ // Domain filtering on the source URL
200+ try {
201+ const hostname = new URL ( details . url ) . hostname ;
202+ const includeList = items . domain_include . split ( "\n" ) . map ( s => s . trim ( ) . toLowerCase ( ) ) . filter ( Boolean ) ;
203+ const excludeList = items . domain_exclude . split ( "\n" ) . map ( s => s . trim ( ) . toLowerCase ( ) ) . filter ( Boolean ) ;
204+
205+ if ( includeList . length > 0 ) {
206+ if ( ! domainMatches ( hostname . toLowerCase ( ) , includeList ) ) return ;
207+ } else if ( excludeList . length > 0 ) {
208+ if ( domainMatches ( hostname . toLowerCase ( ) , excludeList ) ) return ;
209+ }
210+ } catch ( _e ) {
211+ // If URL parsing fails, proceed anyway
212+ }
213+
214+ const data = {
215+ edge_type : "redirect" ,
216+ url : details . url ,
217+ redirect_url : details . redirectUrl ,
218+ ts : details . timeStamp ,
219+ ip : details . ip ,
220+ method : details . method ,
221+ status : details . statusCode ,
222+ type : details . type
223+ } ;
224+
225+ try {
226+ await fetch ( url_backend , {
227+ method : 'POST' ,
228+ headers : { 'Content-Type' : 'application/json' } ,
229+ body : JSON . stringify ( data ) + "\r\n"
230+ } ) ;
231+ } catch ( error ) {
232+ console . error ( "HTTP Graph Error: Could not send redirect data to backend." , error ) ;
233+ }
234+
235+ broadcastToViewers ( data ) ;
236+ } ,
237+ requestFilter ,
238+ [ "responseHeaders" ]
239+ ) ;
240+
241+ chrome . webRequest . onErrorOccurred . addListener (
242+ ( details ) => {
243+ requestTimings . delete ( details . requestId ) ;
244+ requestInitiators . delete ( details . requestId ) ;
245+ } ,
246+ requestFilter
247+ ) ;
248+
101249chrome . runtime . onInstalled . addListener ( ( ) => {
102250 chrome . storage . local . set ( {
103251 rest_port : default_rest_port ,
104- scrub_parameters : default_scrub_parameters
252+ scrub_parameters : default_scrub_parameters ,
253+ collecting : default_collecting ,
254+ domain_include : default_domain_include ,
255+ domain_exclude : default_domain_exclude
256+ } ) ;
257+ updateBadge ( default_collecting ) ;
258+ } ) ;
259+
260+ chrome . runtime . onStartup . addListener ( async ( ) => {
261+ const items = await chrome . storage . local . get ( { collecting : default_collecting } ) ;
262+ updateBadge ( items . collecting ) ;
263+ } ) ;
264+
265+ // ── Live Viewer Streaming ──
266+ chrome . runtime . onConnectExternal . addListener ( ( port ) => {
267+ if ( port . name !== "httpgraph-viewer" ) return ;
268+ console . log ( "HTTP Graph: Viewer connected" ) ;
269+ viewerPorts . add ( port ) ;
270+ port . onDisconnect . addListener ( ( ) => {
271+ viewerPorts . delete ( port ) ;
272+ console . log ( "HTTP Graph: Viewer disconnected" ) ;
105273 } ) ;
106- } ) ;
274+ } ) ;
0 commit comments