feature: 新增 `pac.txt`(即 `GFW` 列表)自动更新功能 (#352)
							parent
							
								
									116e7c778d
								
							
						
					
					
						commit
						2d8243692c
					
				|  | @ -42,7 +42,8 @@ module.exports = { | |||
|   }, | ||||
|   pac: { | ||||
|     enabled: true, | ||||
|     // update: [ 'https://gitlab.com/gfwlist/gfwlist/raw/master/gfwlist.txt' ],
 | ||||
|     autoUpdate: true, | ||||
|     pacFileUpdateUrl: 'https://gitlab.com/gfwlist/gfwlist/raw/master/gfwlist.txt', | ||||
|     pacFileAbsolutePath: null, // 自定义 pac.txt 文件位置,可以是本地文件路径
 | ||||
|     pacFilePath: './extra/pac/pac.txt' // 内置 pac.txt 文件路径
 | ||||
|   } | ||||
|  |  | |||
|  | @ -62,10 +62,10 @@ | |||
|           <div class="form-help">某些库需要自己设置镜像变量,才能下载,比如:electron</div> | ||||
|           <a-row :gutter="10" style="margin-top: 5px" v-for="(item,index) of npmVariables" :key='index'> | ||||
|             <a-col :span="10"> | ||||
|               <a-input v-model="item.key" :title="item.key" :readonly="true"></a-input> | ||||
|               <a-input v-model="item.key" :title="item.key" readOnly></a-input> | ||||
|             </a-col> | ||||
|             <a-col :span="10"> | ||||
|               <a-input v-model="item.value" :title="item.value" :readonly="true"></a-input> | ||||
|               <a-input v-model="item.value" :title="item.value" readOnly></a-input> | ||||
|             </a-col> | ||||
|             <a-col :span="4"> | ||||
|               <a-icon v-if="item.exists && item.hadSet" title="已设置" style="color:green" type="check"/> | ||||
|  |  | |||
|  | @ -19,12 +19,29 @@ | |||
|             声明:仅供技术学习与探讨! | ||||
|           </div> | ||||
|         </a-form-item> | ||||
|         <hr/> | ||||
|         <a-form-item label="PAC" :label-col="labelCol" :wrapper-col="wrapperCol"> | ||||
|           <a-checkbox v-model="config.plugin.overwall.pac.enabled"> | ||||
|             启用PAC | ||||
|           </a-checkbox> | ||||
|           <div class="form-help">PAC内收录了常见的被封杀的域名,当里面某些域名你不想被拦截时,可以关闭PAC</div> | ||||
|         </a-form-item> | ||||
|         <a-form-item label="自动更新PAC" :label-col="labelCol" :wrapper-col="wrapperCol"> | ||||
|           <a-checkbox v-model="config.plugin.overwall.pac.autoUpdate"> | ||||
|             是否自动更新PAC | ||||
|           </a-checkbox> | ||||
|           <div class="form-help"> | ||||
|             开启自动更新后,启动代理服务时,将会异步从下面的远程地址下载PAC文件到本地。<br/> | ||||
|             注:只要下载成功后,即使关闭自动更新功能,也会优先读取最近下载的PAC文件! | ||||
|           </div> | ||||
|         </a-form-item> | ||||
|         <a-form-item label="远程PAC文件地址" :label-col="labelCol" :wrapper-col="wrapperCol"> | ||||
|           <a-input v-model="config.plugin.overwall.pac.pacFileUpdateUrl"/> | ||||
|           <div class="form-help"> | ||||
|             远程PAC文件内容可以是 base64 编码格式,也可以是未经过编码的 | ||||
|           </div> | ||||
|         </a-form-item> | ||||
|         <hr/> | ||||
|         <a-form-item label="自定义域名" :label-col="labelCol" :wrapper-col="wrapperCol"> | ||||
|           <div> | ||||
|             <a-row :gutter="10" style=""> | ||||
|  |  | |||
|  | @ -1,8 +1,12 @@ | |||
| const url = require('url') | ||||
| const request = require('request') | ||||
| const lodash = require('lodash') | ||||
| const pac = require('./source/pac') | ||||
| const matchUtil = require('../../../utils/util.match') | ||||
| const log = require('../../../utils/util.log') | ||||
| const path = require('path') | ||||
| const fs = require('fs') | ||||
| const { Buffer } = require('buffer') | ||||
| let pacClient = null | ||||
| 
 | ||||
| function matched (hostname, overWallTargetMap) { | ||||
|  | @ -23,7 +27,93 @@ function matched (hostname, overWallTargetMap) { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports = function createOverWallIntercept (overWallConfig) { | ||||
| function getUserBasePath () { | ||||
|   const userHome = process.env.USERPROFILE || process.env.HOME || '/' | ||||
|   return path.resolve(userHome, './.dev-sidecar') | ||||
| } | ||||
| 
 | ||||
| // 下载的 pac.txt 文件保存路径
 | ||||
| function getTmpPacFilePath () { | ||||
|   return path.join(getUserBasePath(), '/pac.txt') | ||||
| } | ||||
| 
 | ||||
| function loadPacLastModifiedTime (pacTxt) { | ||||
|   const matched = pacTxt.match(/(?<=! Last Modified: )[^\n]+/g) | ||||
|   if (matched && matched.length > 0) { | ||||
|     try { | ||||
|       return new Date(matched[0]) | ||||
|     } catch (ignore) { | ||||
|       return null | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 保存 pac 内容到 `~/pac.txt` 文件中
 | ||||
| function savePacFile (pacTxt) { | ||||
|   const pacFilePath = getTmpPacFilePath() | ||||
|   fs.writeFileSync(pacFilePath, pacTxt) | ||||
|   log.info('保存 pac.txt 文件成功:', pacFilePath) | ||||
| 
 | ||||
|   // 尝试解析和修改 pac.txt 文件时间
 | ||||
|   const lastModifiedTime = loadPacLastModifiedTime(pacTxt) | ||||
|   if (lastModifiedTime) { | ||||
|     fs.stat(pacFilePath, (err, stats) => { | ||||
|       if (err) { | ||||
|         log.error('修改 pac.txt 文件时间失败:', err) | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       // 修改文件的访问时间和修改时间为当前时间
 | ||||
|       fs.utimes(pacFilePath, lastModifiedTime, lastModifiedTime, (utimesErr) => { | ||||
|         if (utimesErr) { | ||||
|           log.error('修改 pac.txt 文件时间失败:', utimesErr) | ||||
|         } else { | ||||
|           log.info(`${pacFilePath} 文件时间已被修改其最近更新时间 '${lastModifiedTime}'`) | ||||
|         } | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   return pacFilePath | ||||
| } | ||||
| 
 | ||||
| // 异步下载 pac.txt ,避免影响代理服务的启动速度
 | ||||
| async function downloadPacAsync (pacConfig) { | ||||
|   const remotePacFileUrl = pacConfig.pacFileUpdateUrl | ||||
|   log.info('开始下载远程 pac.txt 文件:', remotePacFileUrl) | ||||
|   request(remotePacFileUrl, (error, response, body) => { | ||||
|     if (error) { | ||||
|       log.error('下载远程 pac.txt 文件失败, error:', error, ', response:', response, ', body:', body) | ||||
|       return | ||||
|     } | ||||
|     if (response && response.statusCode === 200) { | ||||
|       if (body == null || body.length < 100) { | ||||
|         log.warn('下载远程 pac.txt 文件成功,但内容为空或内容太短,判断为无效的 pax.txt 文件:', remotePacFileUrl, ', body:', body) | ||||
|         return | ||||
|       } else { | ||||
|         log.info('下载远程 pac.txt 文件成功:', remotePacFileUrl) | ||||
|       } | ||||
| 
 | ||||
|       // 尝试解析Base64(注:https://gitlab.com/gfwlist/gfwlist/raw/master/gfwlist.txt 下载下来的是Base64格式)
 | ||||
|       let pacTxt = body | ||||
|       try { | ||||
|         pacTxt = Buffer.from(pacTxt, 'base64').toString('utf8') | ||||
|       } catch (e) { | ||||
|         if (pacTxt.indexOf('||') < 0) { // TODO: 待优化,需要判断下载的 pac.txt 文件内容是否正确,目前暂时先简单判断一下
 | ||||
|           log.error(`远程 pac.txt 文件内容即不是base64格式,也不是要求的格式,url: ${remotePacFileUrl},body: ${body}`) | ||||
|           return | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // 保存到本地
 | ||||
|       savePacFile(pacTxt) | ||||
|     } else { | ||||
|       log.error('下载远程 pac.txt 文件失败, response:', response, ', body:', body) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| function createOverwallMiddleware (overWallConfig) { | ||||
|   if (!overWallConfig || overWallConfig.enabled !== true) { | ||||
|     return null | ||||
|   } | ||||
|  | @ -109,3 +199,9 @@ module.exports = function createOverWallIntercept (overWallConfig) { | |||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|   getTmpPacFilePath, | ||||
|   downloadPacAsync, | ||||
|   createOverwallMiddleware | ||||
| } | ||||
|  |  | |||
|  | @ -3,9 +3,10 @@ const dnsUtil = require('./lib/dns') | |||
| const log = require('./utils/util.log') | ||||
| const matchUtil = require('./utils/util.match') | ||||
| const path = require('path') | ||||
| const fs = require('fs') | ||||
| const scriptInterceptor = require('./lib/interceptor/impl/res/script') | ||||
| 
 | ||||
| const createOverwallMiddleware = require('./lib/proxy/middleware/overwall') | ||||
| const { getTmpPacFilePath, downloadPacAsync, createOverwallMiddleware } = require('./lib/proxy/middleware/overwall') | ||||
| 
 | ||||
| // 处理拦截配置
 | ||||
| function buildIntercepts (intercepts) { | ||||
|  | @ -15,12 +16,11 @@ function buildIntercepts (intercepts) { | |||
|   return intercepts | ||||
| } | ||||
| 
 | ||||
| module.exports = (config) => { | ||||
|   const intercepts = matchUtil.domainMapRegexply(buildIntercepts(config.intercepts)) | ||||
|   const whiteList = matchUtil.domainMapRegexply(config.whiteList) | ||||
| module.exports = (serverConfig) => { | ||||
|   const intercepts = matchUtil.domainMapRegexply(buildIntercepts(serverConfig.intercepts)) | ||||
|   const whiteList = matchUtil.domainMapRegexply(serverConfig.whiteList) | ||||
| 
 | ||||
|   const dnsMapping = config.dns.mapping | ||||
|   const serverConfig = config | ||||
|   const dnsMapping = serverConfig.dns.mapping | ||||
|   const setting = serverConfig.setting | ||||
| 
 | ||||
|   if (!setting.script.dirAbsolutePath) { | ||||
|  | @ -30,16 +30,35 @@ module.exports = (config) => { | |||
|     setting.verifySsl = true | ||||
|   } | ||||
| 
 | ||||
|   const overwallConfig = serverConfig.plugin.overwall | ||||
|   if (!overwallConfig.pac.pacFileAbsolutePath) { | ||||
|     overwallConfig.pac.pacFileAbsolutePath = path.join(setting.rootDir, overwallConfig.pac.pacFilePath) | ||||
|   const overWallConfig = serverConfig.plugin.overwall | ||||
|   if (overWallConfig.pac && overWallConfig.pac.enabled) { | ||||
|     const pacConfig = overWallConfig.pac | ||||
| 
 | ||||
|     // 自动更新 pac.txt
 | ||||
|     if (!pacConfig.pacFileAbsolutePath && pacConfig.autoUpdate) { | ||||
|       // 异步下载远程 pac.txt 文件,并保存到本地;下载成功后,需要重启代理服务才会生效
 | ||||
|       downloadPacAsync(pacConfig) | ||||
|     } | ||||
| 
 | ||||
|     // 优先使用本地已下载的 pac.txt 文件
 | ||||
|     if (!pacConfig.pacFileAbsolutePath && fs.existsSync(getTmpPacFilePath())) { | ||||
|       pacConfig.pacFileAbsolutePath = getTmpPacFilePath() | ||||
|       log.info('读取已下载的 pac.txt 文件:', pacConfig.pacFileAbsolutePath) | ||||
|     } | ||||
| 
 | ||||
|     if (!pacConfig.pacFileAbsolutePath) { | ||||
|       pacConfig.pacFileAbsolutePath = path.join(setting.rootDir, pacConfig.pacFilePath) | ||||
|       if (pacConfig.autoUpdate) { | ||||
|         log.warn('远程 pac.txt 文件下载失败或还在下载中,现使用内置 pac.txt 文件:', pacConfig.pacFileAbsolutePath) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // 插件列表
 | ||||
|   const middlewares = [] | ||||
| 
 | ||||
|   // 梯子插件:如果启用了,则添加到插件列表中
 | ||||
|   const overwallMiddleware = createOverwallMiddleware(overwallConfig) | ||||
|   const overwallMiddleware = createOverwallMiddleware(overWallConfig) | ||||
|   if (overwallMiddleware) { | ||||
|     middlewares.push(overwallMiddleware) | ||||
|   } | ||||
|  | @ -48,9 +67,9 @@ module.exports = (config) => { | |||
|     host: serverConfig.host, | ||||
|     port: serverConfig.port, | ||||
|     dnsConfig: { | ||||
|       providers: dnsUtil.initDNS(serverConfig.dns.providers, matchUtil.domainMapRegexply(config.preSetIpList)), | ||||
|       providers: dnsUtil.initDNS(serverConfig.dns.providers, matchUtil.domainMapRegexply(serverConfig.preSetIpList)), | ||||
|       mapping: matchUtil.domainMapRegexply(dnsMapping), | ||||
|       speedTest: config.dns.speedTest | ||||
|       speedTest: serverConfig.dns.speedTest | ||||
|     }, | ||||
|     setting, | ||||
|     sniConfig: serverConfig.sniList, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 王良
						王良