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) { const ret1 = matchUtil.matchHostname(overWallTargetMap, hostname, 'matched overwall') if (ret1) { return true } if (pacClient == null) { return false } const ret = pacClient.FindProxyForURL('https://' + hostname, hostname) if (ret && ret.indexOf('PROXY ') === 0) { log.info(`matchHostname: matched overwall: '${hostname}' -> '${ret}' in pac.txt`) return true } else { log.debug(`matchHostname: matched overwall: Not-Matched '${hostname}' -> '${ret}' in pac.txt`) return false } } 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 } } } function formatDate (date) { const year = date.getFullYear() const month = (date.getMonth() + 1).toString().padStart(2, '0') const day = date.getDate().toString().padStart(2, '0') const hours = date.getHours().toString().padStart(2, '0') const minutes = date.getMinutes().toString().padStart(2, '0') const seconds = date.getSeconds().toString().padStart(2, '0') return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } // 保存 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}' 文件的修改时间已更新为其最近更新时间 '${formatDate(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 } if (overWallConfig.pac && overWallConfig.pac.enabled) { // 初始化pac pacClient = pac.createPacClient(overWallConfig.pac.pacFileAbsolutePath) } let server = overWallConfig.server let keys = Object.keys(server) if (keys.length === 0) { server = overWallConfig.serverDefault keys = Object.keys(server) } if (keys.length === 0) { return null } const overWallTargetMap = matchUtil.domainMapRegexply(overWallConfig.targets) return { sslConnectInterceptor: (req, cltSocket, head) => { const hostname = req.url.split(':')[0] return matched(hostname, overWallTargetMap) }, requestIntercept (context, req, res, ssl, next) { const { rOptions, log, RequestCounter } = context if (rOptions.protocol === 'http:') { return } const hostname = rOptions.hostname if (!matched(hostname, overWallTargetMap)) { return } const cacheKey = '__over_wall_proxy__' let proxyServer = keys[0] if (RequestCounter && keys.length > 1) { const count = RequestCounter.getOrCreate(cacheKey, keys) if (count.value == null) { count.doRank() } if (count.value == null) { log.error('`count.value` is null, the count:', count) } else { count.doCount(count.value) proxyServer = count.value context.requestCount = { key: cacheKey, value: count.value, count } } } const domain = proxyServer const port = server[domain].port const path = server[domain].path const password = server[domain].password const proxyTarget = domain + '/' + path + '/' + hostname + req.url // const backup = interceptOpt.backup const proxy = proxyTarget.indexOf('http:') === 0 || proxyTarget.indexOf('https:') === 0 ? proxyTarget : (rOptions.protocol + '//' + proxyTarget) // eslint-disable-next-line node/no-deprecated-api const URL = url.parse(proxy) rOptions.origional = lodash.cloneDeep(rOptions) // 备份原始请求参数 delete rOptions.origional.agent delete rOptions.origional.headers rOptions.protocol = URL.protocol rOptions.hostname = URL.host rOptions.host = URL.host rOptions.headers.host = URL.host if (password) { rOptions.headers.dspassword = password } rOptions.path = URL.path if (URL.port == null) { rOptions.port = port || (rOptions.protocol === 'https:' ? 443 : 80) } log.info('OverWall:', rOptions.hostname, '➜', proxyTarget) if (context.requestCount) { log.debug('OverWall choice:', JSON.stringify(context.requestCount)) } return true } } } module.exports = { getTmpPacFilePath, downloadPacAsync, createOverwallMiddleware }