@@ -5,6 +5,7 @@ const fs = require('node:fs')
55const path = require ( 'node:path' )
66const request = require ( 'request' )
77const Registry = require ( 'winreg' )
8+ const sudoPrompt = require ( '@vscode/sudo-prompt' )
89const log = require ( '../../../utils/util.log.core' )
910const Shell = require ( '../../shell' )
1011const extraPath = require ( '../extra-path' )
@@ -183,6 +184,151 @@ function getProxyExcludeIpStr (split) {
183184 return excludeIpStr
184185}
185186
187+ function parseMacNetworkServiceByDevice ( networkServiceOrder , device ) {
188+ if ( ! networkServiceOrder || ! device ) {
189+ return null
190+ }
191+ const lines = networkServiceOrder . split ( / \r ? \n / )
192+ for ( let i = 0 ; i < lines . length ; i ++ ) {
193+ if ( lines [ i ] . includes ( `Device: ${ device } ` ) ) {
194+ for ( let j = i - 1 ; j >= 0 ; j -- ) {
195+ const serviceLine = lines [ j ] . trim ( )
196+ const markerIndex = serviceLine . indexOf ( ') ' )
197+ if ( serviceLine . startsWith ( '(' ) && markerIndex > 0 ) {
198+ return serviceLine . slice ( markerIndex + 2 ) . trim ( )
199+ }
200+ }
201+ }
202+ }
203+ return null
204+ }
205+
206+ function parseMacRouteDevice ( routeOutput ) {
207+ if ( ! routeOutput ) {
208+ return null
209+ }
210+ const routeLines = routeOutput . split ( / \r ? \n / )
211+ for ( const routeLine of routeLines ) {
212+ const trimmedLine = routeLine . trim ( )
213+ if ( trimmedLine . startsWith ( 'interface:' ) ) {
214+ return trimmedLine . slice ( 'interface:' . length ) . trim ( ) || null
215+ }
216+ }
217+ return null
218+ }
219+
220+ function pickMacNetworkService ( listAllNetworkServicesOutput ) {
221+ if ( ! listAllNetworkServicesOutput ) {
222+ return null
223+ }
224+ const services = listAllNetworkServicesOutput
225+ . split ( / \r ? \n / )
226+ . map ( item => item . replace ( / ^ \* / , '' ) . trim ( ) )
227+ . filter ( item => item && ! item . startsWith ( 'An asterisk (*) denotes' ) )
228+ if ( services . length === 0 ) {
229+ return null
230+ }
231+ const preferredServices = [ 'Wi-Fi' , 'WiFi' , 'Ethernet' ]
232+ for ( const preferredService of preferredServices ) {
233+ const matched = services . find ( item => item === preferredService )
234+ if ( matched ) {
235+ return matched
236+ }
237+ }
238+ return services [ 0 ]
239+ }
240+
241+ async function getMacNetworkService ( exec ) {
242+ try {
243+ const routeOutput = await exec ( 'route -n get 0.0.0.0' )
244+ const device = parseMacRouteDevice ( routeOutput )
245+ if ( device ) {
246+ log . info ( 'macOS 代理服务检测:当前网络设备:' , device )
247+ try {
248+ const networkServiceOrder = await exec ( 'networksetup -listnetworkserviceorder' )
249+ const matchedService = parseMacNetworkServiceByDevice ( networkServiceOrder , device )
250+ if ( matchedService ) {
251+ log . info ( 'macOS 代理服务检测:通过设备名匹配到网络服务:' , matchedService )
252+ return matchedService
253+ }
254+ log . warn ( 'macOS 代理服务检测:未通过设备名匹配到网络服务,尝试备用方法' )
255+ } catch ( e ) {
256+ log . warn ( 'macOS 代理服务检测:获取网络服务列表失败:' , e . message , ',尝试备用方法' )
257+ }
258+ } else {
259+ log . warn ( 'macOS 代理服务检测:未检测到当前网络设备,尝试备用方法' )
260+ }
261+ } catch ( e ) {
262+ log . warn ( 'macOS 代理服务检测:获取路由信息失败:' , e . message , ',尝试备用方法' )
263+ }
264+
265+ try {
266+ const allServicesOutput = await exec ( 'networksetup -listallnetworkservices' )
267+ const fallbackService = pickMacNetworkService ( allServicesOutput )
268+ if ( fallbackService ) {
269+ log . info ( 'macOS 代理服务检测:通过服务列表备用方法找到网络服务:' , fallbackService )
270+ return fallbackService
271+ }
272+ log . warn ( 'macOS 代理服务检测:未通过服务列表找到可用网络服务' )
273+ } catch ( e ) {
274+ log . warn ( 'macOS 代理服务检测:获取所有网络服务列表失败:' , e . message )
275+ }
276+
277+ throw new Error ( '未找到可用的 macOS 网络服务,无法设置系统代理' )
278+ }
279+
280+ // macOS exit code 14 = "You don't have permission to change the system preferences."
281+ const MACOS_NETWORKSETUP_PERMISSION_ERROR_CODE = 14
282+
283+ /**
284+ * POSIX single-quote escaping: wraps `arg` in single quotes, escaping any
285+ * embedded single quotes with the '\''-idiom. This prevents shell
286+ * metacharacter expansion regardless of the character set of the value.
287+ * @param {string|number } arg
288+ * @returns {string }
289+ */
290+ function shellEscapeArg ( arg ) {
291+ return "'" + String ( arg ) . replace ( / ' / g, "'\\''" ) + "'"
292+ }
293+
294+ /**
295+ * Strict-validate a proxy host (IPv4 / IPv6 / hostname) and throw if the
296+ * value looks suspicious. This is a defence-in-depth guard for the sudo
297+ * execution path; the primary protection is `shellEscapeArg`.
298+ */
299+ function validateProxyIp ( ip ) {
300+ if ( typeof ip !== 'string' || ! / ^ [ \w . \- : [ \] ] + $ / . test ( ip ) ) {
301+ throw new Error ( `无效的代理 IP 地址: ${ ip } ` )
302+ }
303+ }
304+
305+ /**
306+ * Strict-validate a TCP port number.
307+ */
308+ function validateProxyPort ( port ) {
309+ const n = Number ( port )
310+ if ( ! Number . isInteger ( n ) || n < 1 || n > 65535 ) {
311+ throw new Error ( `无效的代理端口号: ${ port } ` )
312+ }
313+ }
314+
315+ function sudoExecMac ( cmd ) {
316+ return new Promise ( ( resolve , reject ) => {
317+ log . info ( '以管理员权限执行命令:' , cmd )
318+ sudoPrompt . exec ( cmd , { name : 'dev-sidecar' } , ( error , stdout , stderr ) => {
319+ if ( stderr ) {
320+ log . warn ( '以管理员权限执行命令,stderr:' , stderr )
321+ }
322+ if ( error ) {
323+ log . error ( '以管理员权限执行命令失败:' , error )
324+ reject ( error )
325+ } else {
326+ resolve ( stdout )
327+ }
328+ } )
329+ } )
330+ }
331+
186332const executor = {
187333 async windows ( exec , params = { } ) {
188334 const { ip, port, setEnv } = params
@@ -324,51 +470,56 @@ const executor = {
324470 }
325471 } ,
326472 async mac ( exec , params = { } ) {
327- // exec = _exec
328- let wifiAdaptor = await exec ( 'sh -c "networksetup -listnetworkserviceorder | grep `route -n get 0.0.0.0 | grep \'interface\' | cut -d \':\' -f2` -B 1 | head -n 1 "' )
329- wifiAdaptor = wifiAdaptor . trim ( )
330- wifiAdaptor = wifiAdaptor . substring ( wifiAdaptor . indexOf ( ' ' ) ) . trim ( )
473+ const wifiAdaptor = await getMacNetworkService ( exec )
331474 const { ip, port } = params
475+
476+ let cmds
332477 if ( ip != null ) { // 设置代理
333478 // 延迟加载config
334479 loadConfig ( )
335480
336481 // https
337- await exec ( `networksetup -setsecurewebproxy "${ wifiAdaptor } " ${ ip } ${ port } ` )
482+ cmds = [ `networksetup -setsecurewebproxy "${ wifiAdaptor } " ${ ip } ${ port } ` ]
338483 // http
339484 if ( config . get ( ) . proxy . proxyHttp ) {
340- await exec ( `networksetup -setwebproxy "${ wifiAdaptor } " ${ ip } ${ port - 1 } ` )
485+ cmds . push ( `networksetup -setwebproxy "${ wifiAdaptor } " ${ ip } ${ port - 1 } ` )
341486 } else {
342- await exec ( `networksetup -setwebproxystate "${ wifiAdaptor } " off` )
487+ cmds . push ( `networksetup -setwebproxystate "${ wifiAdaptor } " off` )
343488 }
344489
345490 // 设置排除域名
346491 const excludeIpStr = getProxyExcludeIpStr ( '" "' )
347- await exec ( `networksetup -setproxybypassdomains "${ wifiAdaptor } " "${ excludeIpStr } "` )
348-
349- // const setEnv = `cat <<ENDOF >> ~/.zshrc
350- // export http_proxy="http://${ip}:${port}"
351- // export https_proxy="http://${ip}:${port}"
352- // ENDOF
353- // source ~/.zshrc
354- // `
355- // await exec(setEnv)
492+ cmds . push ( `networksetup -setproxybypassdomains "${ wifiAdaptor } " "${ excludeIpStr } "` )
356493 } else { // 关闭代理
357- // https
358- await exec ( `networksetup -setsecurewebproxystate "${ wifiAdaptor } " off` )
359- // http
360- await exec ( `networksetup -setwebproxystate "${ wifiAdaptor } " off` )
361-
362- // const removeEnv = `
363- // sed -ie '/export http_proxy/d' ~/.zshrc
364- // sed -ie '/export https_proxy/d' ~/.zshrc
365- // source ~/.zshrc
366- // `
367- // await exec(removeEnv)
494+ // https + http
495+ cmds = [
496+ `networksetup -setsecurewebproxystate "${ wifiAdaptor } " off` ,
497+ `networksetup -setwebproxystate "${ wifiAdaptor } " off` ,
498+ ]
499+ }
500+
501+ // 先尝试直接执行;若因权限不足(exit code 14)失败,弹出系统授权对话框后重试
502+ try {
503+ for ( const cmd of cmds ) {
504+ await exec ( cmd )
505+ }
506+ } catch ( e ) {
507+ if ( e . code === MACOS_NETWORKSETUP_PERMISSION_ERROR_CODE ) {
508+ log . warn ( 'networksetup 命令需要管理员权限(exit code 14),正在弹出系统授权对话框...' )
509+ await sudoExecMac ( cmds . join ( ' && ' ) )
510+ log . info ( '以管理员权限执行 networksetup 命令成功' )
511+ } else {
512+ throw e
513+ }
368514 }
369515 } ,
370516}
371517
372- module . exports = async function ( args ) {
518+ const setSystemProxy = async function ( args ) {
373519 return execute ( executor , args )
374520}
521+
522+ module . exports = setSystemProxy
523+ module . exports . parseMacNetworkServiceByDevice = parseMacNetworkServiceByDevice
524+ module . exports . parseMacRouteDevice = parseMacRouteDevice
525+ module . exports . pickMacNetworkService = pickMacNetworkService
0 commit comments