dev-sidecar/packages/mitmproxy/src/lib/proxy/middleware/overwall.js

218 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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)
}
// 尝试解析Base64https://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
}