1
1
import debug from 'debug'
2
2
import browser from 'webextension-polyfill'
3
3
import { CompanionState } from '../../types/companion.js'
4
+ import { IFilter , IRegexFilter , RegexFilter } from './baseRegexFilter.js'
5
+ import { CommonPatternRedirectRegexFilter } from './commonPatternRedirectRegexFilter.js'
6
+ import { NamespaceRedirectRegexFilter } from './namespaceRedirectRegexFilter.js'
7
+ import { SubdomainRedirectRegexFilter } from './subdomainRedirectRegexFilter.js'
4
8
5
9
// this won't work in webworker context. Needs to be enabled manually
6
10
// https://github.com/debug-js/debug/issues/916
7
11
const log = debug ( 'ipfs-companion:redirect-handler:blockOrObserve' )
8
12
log . error = debug ( 'ipfs-companion:redirect-handler:blockOrObserve:error' )
9
13
14
+ export const DEFAULT_NAMESPACES = new Set ( [ 'ipfs' , 'ipns' ] )
15
+
10
16
export const GLOBAL_STATE_CHANGE = 'GLOBAL_STATE_CHANGE'
11
17
export const GLOBAL_STATE_OPTION_CHANGE = 'GLOBAL_STATE_OPTION_CHANGE'
12
18
export const DELETE_RULE_REQUEST = 'DELETE_RULE_REQUEST'
13
19
export const DELETE_RULE_REQUEST_SUCCESS = 'DELETE_RULE_REQUEST_SUCCESS'
20
+
21
+ // We need to match the rest of the URL, so we can use a wildcard.
14
22
export const RULE_REGEX_ENDING = '((?:[^\\.]|$).*)$'
15
23
16
24
interface regexFilterMap {
@@ -21,6 +29,7 @@ interface regexFilterMap {
21
29
interface redirectHandlerInput {
22
30
originUrl : string
23
31
redirectUrl : string
32
+ getPort : ( state : CompanionState ) => string
24
33
}
25
34
26
35
type messageToSelfType = typeof GLOBAL_STATE_CHANGE | typeof GLOBAL_STATE_OPTION_CHANGE | typeof DELETE_RULE_REQUEST
@@ -29,10 +38,16 @@ interface messageToSelf {
29
38
value ?: string | Record < string , unknown >
30
39
}
31
40
41
+ export const defaultNSRegexStr = `(${ [ ...DEFAULT_NAMESPACES ] . join ( '|' ) } )`
42
+
32
43
// We need to check if the browser supports the declarativeNetRequest API.
33
44
// TODO: replace with check for `Blocking` in `chrome.webRequest.OnBeforeRequestOptions`
34
45
// which is currently a bug https://bugs.chromium.org/p/chromium/issues/detail?id=1427952
35
- export const supportsBlock = ! ( browser . declarativeNetRequest ?. MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES === 5000 )
46
+ // this needs to be a function call, because in tests we mock browser.declarativeNetRequest
47
+ // the way sinon ends up stubbing it, it's not directly available in the global scope on import
48
+ // rather it gets replaced dynamically when the module is imported. Which means, we can't
49
+ // just check for the existence of the property, we need to call the browser instance at that point.
50
+ export const supportsBlock = ( ) : boolean => ! ( browser . declarativeNetRequest ?. MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES > 0 )
36
51
37
52
/**
38
53
* Notify self about state change.
@@ -61,21 +76,28 @@ export async function notifyDeleteRule (id: number): Promise<void> {
61
76
*/
62
77
async function sendMessageToSelf ( msg : messageToSelfType , value ?: any ) : Promise < void > {
63
78
// this check ensures we don't send messages to ourselves if blocking mode is enabled.
64
- if ( ! supportsBlock ) {
65
- const message : messageToSelf = { type : msg , value }
66
- await browser . runtime . sendMessage ( { message } )
79
+ if ( ! supportsBlock ( ) ) {
80
+ const message : messageToSelf = { type : msg }
81
+ await browser . runtime . sendMessage ( message )
67
82
}
68
83
}
69
84
70
85
const savedRegexFilters : Map < string , regexFilterMap > = new Map ( )
71
86
const DEFAULT_LOCAL_RULES : redirectHandlerInput [ ] = [
72
87
{
73
88
originUrl : 'http://127.0.0.1' ,
74
- redirectUrl : 'http://localhost'
89
+ redirectUrl : 'http://localhost' ,
90
+ getPort : ( { gwURLString } ) : string => new URL ( gwURLString ) . port
75
91
} ,
76
92
{
77
93
originUrl : 'http://[::1]' ,
78
- redirectUrl : 'http://localhost'
94
+ redirectUrl : 'http://localhost' ,
95
+ getPort : ( { gwURLString } ) : string => new URL ( gwURLString ) . port
96
+ } ,
97
+ {
98
+ originUrl : 'http://localhost' ,
99
+ redirectUrl : 'http://127.0.0.1' ,
100
+ getPort : ( { apiURL } ) : string => new URL ( apiURL ) . port
79
101
}
80
102
]
81
103
@@ -97,10 +119,10 @@ export function isLocalHost (url: string): boolean {
97
119
* @param str URL string to escape
98
120
* @returns
99
121
*/
100
- function escapeURLRegex ( str : string ) : string {
122
+ export function escapeURLRegex ( str : string ) : string {
101
123
// these characters are allowed in the URL, but not in the regex.
102
124
// eslint-disable-next-line no-useless-escape
103
- const ALLOWED_CHARS_URL_REGEX = / ( [: \/ \? # \[ \] @ ! $ & ' \( \ ) \* \+ , ; = - _ \. ~ ] ) / g
125
+ const ALLOWED_CHARS_URL_REGEX = / ( [: \/ \? # \[ \] @ ! $ & ' \( \ ) \* \+ , ; = \ -_ \. ~ ] ) / g
104
126
return str . replace ( ALLOWED_CHARS_URL_REGEX , '\\$1' )
105
127
}
106
128
@@ -111,43 +133,29 @@ function escapeURLRegex (str: string): string {
111
133
* @param redirectUrl
112
134
* @returns
113
135
*/
114
- function constructRegexFilter ( { originUrl, redirectUrl } : redirectHandlerInput ) : {
115
- regexSubstitution : string
116
- regexFilter : string
117
- } {
118
- // We can traverse the URL from the end, and find the first character that is different.
119
- let commonIdx = 1
120
- while ( commonIdx < Math . min ( originUrl . length , redirectUrl . length ) ) {
121
- if ( originUrl [ originUrl . length - commonIdx ] !== redirectUrl [ redirectUrl . length - commonIdx ] ) {
122
- break
136
+ function constructRegexFilter ( { originUrl, redirectUrl } : IRegexFilter ) : IFilter {
137
+ // the order is very important here, because we want to match the best possible filter.
138
+ const filtersToTryInOrder : Array < typeof RegexFilter > = [
139
+ SubdomainRedirectRegexFilter ,
140
+ NamespaceRedirectRegexFilter ,
141
+ CommonPatternRedirectRegexFilter
142
+ ]
143
+
144
+ for ( const Filter of filtersToTryInOrder ) {
145
+ const filter = new Filter ( { originUrl, redirectUrl } )
146
+ if ( filter . canHandle ) {
147
+ return filter . filter
123
148
}
124
- commonIdx += 1
125
- }
126
-
127
- // We can now construct the regex filter and substitution.
128
- let regexSubstitution = redirectUrl . slice ( 0 , redirectUrl . length - commonIdx + 1 ) + '\\1'
129
- // We need to escape the characters that are allowed in the URL, but not in the regex.
130
- const regexFilterFirst = escapeURLRegex ( originUrl . slice ( 0 , originUrl . length - commonIdx + 1 ) )
131
- // We need to match the rest of the URL, so we can use a wildcard.
132
- const RULE_REGEX_ENDING = '((?:[^\\.]|$).*)$'
133
- let regexFilter = `^${ regexFilterFirst } ${ RULE_REGEX_ENDING } ` . replace ( / h t t p s ? / ig, 'https?' )
134
-
135
- // This method does not parse:
136
- // originUrl: "https://awesome.ipfs.io/"
137
- // redirectUrl: "http://localhost:8081/ipns/awesome.ipfs.io/"
138
- // that ends up with capturing all urls which we do not want.
139
- if ( regexFilter === `^https?\\:\\/${ RULE_REGEX_ENDING } ` ) {
140
- const subdomain = new URL ( originUrl ) . hostname
141
- regexFilter = `^https?\\:\\/\\/${ escapeURLRegex ( subdomain ) } ${ RULE_REGEX_ENDING } `
142
- regexSubstitution = regexSubstitution . replace ( '\\1' , `/${ subdomain } \\1` )
143
149
}
144
150
145
- return { regexSubstitution, regexFilter }
151
+ // this is just to satisfy the compiler, this should never happen. Because CommonPatternRedirectRegexFilter can always
152
+ // handle.
153
+ return new CommonPatternRedirectRegexFilter ( { originUrl, redirectUrl } ) . filter
146
154
}
147
155
148
156
// If the browser supports the declarativeNetRequest API, we can block the request.
149
157
export function getExtraInfoSpec < T > ( additionalParams : T [ ] = [ ] ) : T [ ] {
150
- if ( supportsBlock ) {
158
+ if ( supportsBlock ( ) ) {
151
159
return [ 'blocking' as T , ...additionalParams ]
152
160
}
153
161
return additionalParams
@@ -239,15 +247,15 @@ async function reconcileRulesAndRemoveOld (state: CompanionState): Promise<void>
239
247
if ( rules . length === 0 ) {
240
248
// we need to populate old rules.
241
249
for ( const [ regexFilter , { regexSubstitution, id } ] of savedRegexFilters . entries ( ) ) {
242
- addRules . push ( generateRule ( id , regexFilter , regexSubstitution ) )
250
+ addRules . push ( generateAddRule ( id , regexFilter , regexSubstitution ) )
243
251
}
244
252
}
245
253
246
254
// make sure that the default rules are added.
247
- for ( const { originUrl, redirectUrl } of DEFAULT_LOCAL_RULES ) {
248
- const { port } = new URL ( state . gwURLString )
249
- const regexFilter = `^${ escapeURLRegex ( `${ originUrl } :${ port } ` ) } (.*)$ `
250
- const regexSubstitution = `${ redirectUrl } :${ port } \\1`
255
+ for ( const { originUrl, redirectUrl, getPort } of DEFAULT_LOCAL_RULES ) {
256
+ const port = getPort ( state )
257
+ const regexFilter = `^${ escapeURLRegex ( `${ originUrl } :${ port } ` ) } \\/ ${ defaultNSRegexStr } \\/ ${ RULE_REGEX_ENDING } `
258
+ const regexSubstitution = `${ redirectUrl } :${ port } / \\1/\\2 `
251
259
252
260
if ( ! savedRegexFilters . has ( regexFilter ) ) {
253
261
// We need to add the new rule.
@@ -276,7 +284,7 @@ function saveAndGenerateRule (
276
284
const id = Math . floor ( Math . random ( ) * 29999 )
277
285
// We need to save the regex filter and ID to check if the rule already exists later.
278
286
savedRegexFilters . set ( regexFilter , { id, regexSubstitution } )
279
- return generateRule ( id , regexFilter , regexSubstitution , excludedInitiatorDomains )
287
+ return generateAddRule ( id , regexFilter , regexSubstitution , excludedInitiatorDomains )
280
288
}
281
289
282
290
/**
@@ -287,7 +295,7 @@ function saveAndGenerateRule (
287
295
* @param excludedInitiatorDomains - The domains that are excluded from the rule.
288
296
* @returns
289
297
*/
290
- function generateRule (
298
+ export function generateAddRule (
291
299
id : number ,
292
300
regexFilter : string ,
293
301
regexSubstitution : string ,
0 commit comments