@@ -18,29 +18,210 @@ export interface DevtoolsProxyOptions {
18
18
19
19
// https://www.electronjs.org/docs/latest/api/structures/proxy-config
20
20
interface ElectronProxyConfig {
21
- mode : 'direct' | 'auto_detect' | 'pac_script' | 'fixed_servers' | 'system' ;
21
+ mode ? : 'direct' | 'auto_detect' | 'pac_script' | 'fixed_servers' | 'system' ;
22
22
pacScript ?: string ;
23
23
proxyRules ?: string ;
24
24
proxyBypassRules ?: string ;
25
25
}
26
26
27
+ function proxyConfForEnvVars (
28
+ env : Record < string , string | undefined > = process . env
29
+ ) : { map : Map < string , string > ; noProxy : string } {
30
+ const map = new Map < string , string > ( ) ;
31
+ let noProxy = '' ;
32
+ for ( const [ _key , value ] of Object . entries ( env ) ) {
33
+ if ( value === undefined ) continue ;
34
+ const key = _key . toUpperCase ( ) ;
35
+ if ( key . endsWith ( '_PROXY' ) && key !== 'NO_PROXY' ) {
36
+ map . set ( key . replace ( / _ P R O X Y $ / , '' ) . toLowerCase ( ) , value || 'direct://' ) ;
37
+ }
38
+ if ( key === 'NO_PROXY' ) noProxy = value ;
39
+ }
40
+ return { map, noProxy } ;
41
+ }
42
+
43
+ function shouldProxy ( noProxy : string , url : URL ) : boolean {
44
+ if ( ! noProxy ) return true ;
45
+ if ( noProxy === '*' ) return false ;
46
+ for ( const noProxyItem of noProxy . split ( / [ \s , ] / ) ) {
47
+ let { host, port } =
48
+ noProxyItem . match ( / (?< host > .+ ) ( : (?< port > \d + ) $ ) ? / ) ?. groups ?? { } ;
49
+ if ( ! host ) {
50
+ host = noProxyItem ;
51
+ port = '' ;
52
+ }
53
+ if ( port && url . port !== port ) continue ;
54
+ if (
55
+ host === url . hostname ||
56
+ ( host . startsWith ( '*' ) && url . hostname . endsWith ( host . substring ( 1 ) ) )
57
+ )
58
+ return false ;
59
+ }
60
+ return true ;
61
+ }
62
+
63
+ export function proxyForUrl (
64
+ proxyOptions : DevtoolsProxyOptions
65
+ ) : ( url : string ) => string {
66
+ if ( proxyOptions . proxy ) {
67
+ const proxyUrl = proxyOptions . proxy ;
68
+ if ( new URL ( proxyUrl ) . protocol === 'direct:' ) return ( ) => '' ;
69
+ return ( target : string ) => {
70
+ if ( shouldProxy ( proxyOptions . noProxyHosts || '' , new URL ( target ) ) ) {
71
+ return proxyUrl ;
72
+ }
73
+ return '' ;
74
+ } ;
75
+ }
76
+
77
+ if ( proxyOptions . useEnvironmentVariableProxies ) {
78
+ const { map, noProxy } = proxyConfForEnvVars ( ) ;
79
+ return ( target : string ) => {
80
+ const url = new URL ( target ) ;
81
+ const protocol = url . protocol . replace ( / : $ / , '' ) ;
82
+ const combinedNoProxyRules = [ noProxy , proxyOptions . noProxyHosts ]
83
+ . filter ( Boolean )
84
+ . join ( ',' ) ;
85
+ const proxyForProtocol = map . get ( protocol ) ;
86
+ if ( proxyForProtocol && shouldProxy ( combinedNoProxyRules , url ) ) {
87
+ return proxyForProtocol ;
88
+ }
89
+ return '' ;
90
+ } ;
91
+ }
92
+
93
+ return ( ) => '' ;
94
+ }
95
+
27
96
export function translateToElectronProxyConfig (
28
97
proxyOptions : DevtoolsProxyOptions
29
98
) : ElectronProxyConfig {
30
99
if ( proxyOptions . proxy ) {
100
+ const url = new URL ( proxyOptions . proxy ) ;
101
+ if ( url . protocol === 'ssh:' ) {
102
+ throw new Error (
103
+ `Using ssh:// proxies for generic browser proxy usage is not supported (translating '${ redactUrl (
104
+ url
105
+ ) } ')`
106
+ ) ;
107
+ }
108
+ if ( url . username || url . password ) {
109
+ throw new Error (
110
+ `Using authenticated proxies for generic browser proxy usage is not supported (translating '${ redactUrl (
111
+ url
112
+ ) } ')`
113
+ ) ;
114
+ }
115
+ if ( url . protocol . startsWith ( 'pac+' ) ) {
116
+ url . protocol = url . protocol . replace ( 'pac+' , '' ) ;
117
+ return {
118
+ mode : 'pac_script' ,
119
+ pacScript : url . toString ( ) ,
120
+ proxyBypassRules : proxyOptions . noProxyHosts ,
121
+ } ;
122
+ }
123
+ if (
124
+ url . protocol !== 'http:' &&
125
+ url . protocol !== 'https:' &&
126
+ url . protocol !== 'socks5:'
127
+ ) {
128
+ throw new Error (
129
+ `Unsupported proxy protocol (translating '${ redactUrl ( url ) } ')`
130
+ ) ;
131
+ }
132
+ return {
133
+ mode : 'fixed_servers' ,
134
+ proxyRules : url . toString ( ) ,
135
+ proxyBypassRules : proxyOptions . noProxyHosts ,
136
+ } ;
137
+ }
138
+
139
+ if ( proxyOptions . useEnvironmentVariableProxies ) {
140
+ const proxyRules : string [ ] = [ ] ;
141
+ const proxyBypassRules = [ proxyOptions . noProxyHosts ] ;
142
+ const { map, noProxy } = proxyConfForEnvVars ( ) ;
143
+ for ( const [ key , value ] of map ) proxyBypassRules . push ( `${ key } =${ value } ` ) ;
144
+ proxyBypassRules . push ( noProxy ) ;
145
+
146
+ return {
147
+ mode : 'fixed_servers' ,
148
+ proxyBypassRules : proxyBypassRules . filter ( Boolean ) . join ( ',' ) || undefined ,
149
+ proxyRules : proxyRules . join ( ';' ) ,
150
+ } ;
31
151
}
152
+
153
+ return { } ;
154
+ }
155
+
156
+ interface DevtoolsProxyOptionsSecretsInternal {
157
+ username ?: string ;
158
+ password ?: string ;
159
+ sshIdentityKeyPassphrase ?: string ;
32
160
}
33
161
34
162
// These mirror our secrets extraction/merging logic in Compass
35
- export function extractProxySecrets ( proxyOptions : DevtoolsProxyOptions ) : {
36
- proxyOptions : Partial < DevtoolsProxyOptions > ;
163
+ export function extractProxySecrets (
164
+ proxyOptions : Readonly < DevtoolsProxyOptions >
165
+ ) : {
166
+ proxyOptions : DevtoolsProxyOptions ;
37
167
secrets : DevtoolsProxyOptionsSecrets ;
38
- } { }
168
+ } {
169
+ const secrets : DevtoolsProxyOptionsSecretsInternal = { } ;
170
+ if ( proxyOptions . proxy ) {
171
+ const proxyUrl = new URL ( proxyOptions . proxy ) ;
172
+ ( { username : secrets . username , password : secrets . password } = proxyUrl ) ;
173
+ proxyUrl . username = proxyUrl . password = '' ;
174
+ proxyOptions = { ...proxyOptions , proxy : proxyUrl . toString ( ) } ;
175
+ }
176
+ if ( proxyOptions . sshOptions ) {
177
+ secrets . sshIdentityKeyPassphrase =
178
+ proxyOptions . sshOptions . identityKeyPassphrase ;
179
+ proxyOptions = {
180
+ ...proxyOptions ,
181
+ sshOptions : {
182
+ ...proxyOptions . sshOptions ,
183
+ identityKeyPassphrase : undefined ,
184
+ } ,
185
+ } ;
186
+ }
187
+ return {
188
+ secrets : JSON . stringify ( secrets ) ,
189
+ proxyOptions : proxyOptions ,
190
+ } ;
191
+ }
39
192
40
193
export function mergeProxySecrets ( {
41
194
proxyOptions,
42
195
secrets,
43
196
} : {
44
- proxyOptions : Partial < DevtoolsProxyOptions > ;
197
+ proxyOptions : Readonly < DevtoolsProxyOptions > ;
45
198
secrets : DevtoolsProxyOptionsSecrets ;
46
- } ) : DevtoolsProxyOptions { }
199
+ } ) : DevtoolsProxyOptions {
200
+ const parsedSecrets : DevtoolsProxyOptionsSecretsInternal =
201
+ JSON . parse ( secrets ) ;
202
+ if (
203
+ ( parsedSecrets . username || parsedSecrets . password ) &&
204
+ proxyOptions . proxy
205
+ ) {
206
+ const proxyUrl = new URL ( proxyOptions . proxy ) ;
207
+ proxyUrl . username = parsedSecrets . username || '' ;
208
+ proxyUrl . password = parsedSecrets . password || '' ;
209
+ proxyOptions = { ...proxyOptions , proxy : proxyUrl . toString ( ) } ;
210
+ }
211
+ if ( parsedSecrets . sshIdentityKeyPassphrase ) {
212
+ proxyOptions = {
213
+ ...proxyOptions ,
214
+ sshOptions : {
215
+ ...proxyOptions . sshOptions ,
216
+ identityKeyPassphrase : parsedSecrets . sshIdentityKeyPassphrase ,
217
+ } ,
218
+ } ;
219
+ }
220
+ return proxyOptions ;
221
+ }
222
+
223
+ function redactUrl ( urlOrString : URL | string ) : string {
224
+ const url = new URL ( urlOrString . toString ( ) ) ;
225
+ url . username = url . password = '(credential)' ;
226
+ return url . toString ( ) ;
227
+ }
0 commit comments