200 lines
5.6 KiB
JavaScript
200 lines
5.6 KiB
JavaScript
const zlib = require('node:zlib')
|
|
const through = require('through2')
|
|
const log = require('../../../utils/util.log.server')
|
|
|
|
// 编解码器
|
|
const codecMap = {
|
|
gzip: {
|
|
createCompressor: () => zlib.createGzip(),
|
|
createDecompressor: () => zlib.createGunzip(),
|
|
},
|
|
deflate: {
|
|
createCompressor: () => zlib.createDeflate(),
|
|
createDecompressor: () => zlib.createInflate(),
|
|
},
|
|
br: {
|
|
createCompressor: () => zlib.createBrotliCompress(),
|
|
createDecompressor: () => zlib.createBrotliDecompress(),
|
|
},
|
|
}
|
|
const supportedEncodings = Object.keys(codecMap)
|
|
const supportedEncodingsStr = supportedEncodings.join(', ')
|
|
|
|
const httpUtil = {
|
|
// 获取响应内容编码
|
|
getContentEncoding (res) {
|
|
const encoding = res.headers['content-encoding']
|
|
if (encoding) {
|
|
return encoding.toLowerCase()
|
|
}
|
|
return null
|
|
},
|
|
// 获取编解码器
|
|
getCodec (encoding) {
|
|
return codecMap[encoding]
|
|
},
|
|
// 获取支持的编解码器名称字符串
|
|
supportedEncodingsStr () {
|
|
return supportedEncodingsStr
|
|
},
|
|
// 是否HTML代码
|
|
isHtml (res) {
|
|
const contentType = res.headers['content-type']
|
|
return (typeof contentType !== 'undefined') && /text\/html|application\/xhtml\+xml/.test(contentType)
|
|
},
|
|
}
|
|
const HEAD = Buffer.from('</head>')
|
|
const HEAD_UP = Buffer.from('</HEAD>')
|
|
const BODY = Buffer.from('</body>')
|
|
const BODY_UP = Buffer.from('</BODY>')
|
|
|
|
function chunkByteReplace (_this, chunk, enc, callback, append) {
|
|
if (append) {
|
|
if (append.head) {
|
|
const ret = injectScriptIntoHtml([HEAD, HEAD_UP], chunk, append.head)
|
|
if (ret != null) {
|
|
chunk = ret
|
|
}
|
|
}
|
|
if (append.body) {
|
|
const ret = injectScriptIntoHtml([BODY, BODY_UP], chunk, append.body)
|
|
if (ret != null) {
|
|
chunk = ret
|
|
}
|
|
}
|
|
}
|
|
_this.push(chunk)
|
|
callback()
|
|
}
|
|
|
|
function injectScriptIntoHtml (tags, chunk, script) {
|
|
for (const tag of tags) {
|
|
const index = chunk.indexOf(tag)
|
|
if (index < 0) {
|
|
continue
|
|
}
|
|
const scriptBuf = Buffer.from(script)
|
|
const chunkNew = Buffer.alloc(chunk.length + scriptBuf.length)
|
|
chunk.copy(chunkNew, 0, 0, index)
|
|
scriptBuf.copy(chunkNew, index, 0)
|
|
chunk.copy(chunkNew, index + scriptBuf.length, index)
|
|
return chunkNew
|
|
}
|
|
return null
|
|
}
|
|
|
|
function handleResponseHeaders (res, proxyRes) {
|
|
Object.keys(proxyRes.headers).forEach((key) => {
|
|
if (proxyRes.headers[key] !== undefined) {
|
|
// let newkey = key.replace(/^[a-z]|-[a-z]/g, (match) => {
|
|
// return match.toUpperCase()
|
|
// })
|
|
const newkey = key
|
|
if (key === 'content-length') {
|
|
// do nothing
|
|
return
|
|
}
|
|
if (key === 'content-security-policy') {
|
|
// content-security-policy
|
|
let policy = proxyRes.headers[key]
|
|
const reg = /script-src ([^:]*);/i
|
|
const matched = policy.match(reg)
|
|
if (matched) {
|
|
if (!matched[1].includes('self')) {
|
|
policy = policy.replace('script-src', 'script-src \'self\' ')
|
|
}
|
|
}
|
|
res.setHeader(newkey, policy)
|
|
return
|
|
}
|
|
|
|
res.setHeader(newkey, proxyRes.headers[key])
|
|
}
|
|
})
|
|
|
|
res.writeHead(proxyRes.statusCode)
|
|
}
|
|
|
|
const contextPath = '/____ds_script____/'
|
|
const monkey = require('../../monkey')
|
|
|
|
module.exports = {
|
|
requestIntercept (context, req, res, ssl, next) {
|
|
const { rOptions, log, setting } = context
|
|
if (rOptions.path.indexOf(contextPath) !== 0) {
|
|
return
|
|
}
|
|
const urlPath = rOptions.path
|
|
let filename = urlPath.replace(contextPath, '')
|
|
|
|
// 重命名过,向下兼容
|
|
if (filename === 'global') {
|
|
filename = 'tampermonkey'
|
|
}
|
|
|
|
const script = monkey.get(setting.script.defaultDir)[filename]
|
|
// log.info(`urlPath: ${urlPath}, fileName: ${filename}, script: ${script}`)
|
|
|
|
log.info('ds_script, filename:', filename, ', `script != null` =', script != null)
|
|
const now = new Date()
|
|
res.writeHead(200, {
|
|
'DS-Middleware': 'ds_script',
|
|
'Content-Type': 'application/javascript; charset=utf-8',
|
|
'Cache-Control': 'public, max-age=86401, immutable', // 缓存1天
|
|
'Last-Modified': now.toUTCString(),
|
|
'Expires': new Date(now.getTime() + 86400000).toUTCString(), // 缓存1天
|
|
'Date': new Date().toUTCString(),
|
|
})
|
|
res.write(script.script)
|
|
res.end()
|
|
return true
|
|
},
|
|
responseInterceptor (req, res, proxyReq, proxyRes, ssl, next, append) {
|
|
if (append == null || (!append.head && !append.body)) {
|
|
next()
|
|
return
|
|
}
|
|
|
|
const isHtml = httpUtil.isHtml(proxyRes)
|
|
const contentLengthIsZero = (() => {
|
|
return proxyRes.headers['content-length'] === 0
|
|
})()
|
|
if (!isHtml || contentLengthIsZero) {
|
|
next()
|
|
return
|
|
}
|
|
|
|
// 先处理头信息
|
|
handleResponseHeaders(res, proxyRes)
|
|
|
|
// 获取响应内容编码
|
|
const encoding = httpUtil.getContentEncoding(proxyRes)
|
|
if (encoding) {
|
|
// 获取编解码器
|
|
const codec = httpUtil.getCodec(encoding)
|
|
if (codec) {
|
|
proxyRes
|
|
.pipe(codec.createDecompressor()) // 解码
|
|
.pipe(through(function (chunk, enc, callback) {
|
|
// 插入head和body
|
|
chunkByteReplace(this, chunk, enc, callback, append)
|
|
}))
|
|
.pipe(codec.createCompressor()) // 编码
|
|
.pipe(res)
|
|
} else {
|
|
log.error(`InsertScriptMiddleware.responseInterceptor(): 暂不支持编码方式 ${encoding}, 目前支持:`, httpUtil.supportedEncodingsStr())
|
|
}
|
|
} else {
|
|
proxyRes
|
|
.pipe(through(function (chunk, enc, callback) {
|
|
chunkByteReplace(this, chunk, enc, callback, append)
|
|
}))
|
|
.pipe(res)
|
|
}
|
|
|
|
next()
|
|
},
|
|
httpUtil,
|
|
handleResponseHeaders,
|
|
}
|