feature: InsertScriptMiddleware.js 支持编码方式 `deflate` 和 `br`,不再是只有 `gzip` 了 (#299)

pull/300/head
王良 2024-04-17 17:56:05 +08:00 committed by GitHub
parent 3968272843
commit c7cdb14daa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 115 additions and 70 deletions

View File

@ -1,23 +1,47 @@
const log = require('../../../utils/util.log')
const through = require('through2') const through = require('through2')
const zlib = require('zlib') const zlib = require('zlib')
const httpUtil = {} // 编解码器
httpUtil.getCharset = function (res) { const codecMap = {
const contentType = res.getHeader('content-type') gzip: {
const reg = /charset=(.*)/ createCompressor: () => zlib.createGzip(),
const matched = contentType.match(reg) createDecompressor: () => zlib.createGunzip()
if (matched) { },
return matched[1] deflate: {
createCompressor: () => zlib.createDeflate(),
createDecompressor: () => zlib.createInflate()
},
br: {
createCompressor: () => zlib.createBrotliCompress(),
createDecompressor: () => zlib.createBrotliDecompress()
} }
return 'utf-8'
} }
httpUtil.isGzip = function (res) { const supportedEncodings = Object.keys(codecMap)
const contentEncoding = res.headers['content-encoding'] const supportedEncodingsStr = supportedEncodings.join(', ')
return !!(contentEncoding && contentEncoding.toLowerCase() === 'gzip')
} const httpUtil = {
httpUtil.isHtml = function (res) { // 获取响应内容编码
const contentType = res.headers['content-type'] getContentEncoding (res) {
return (typeof contentType !== 'undefined') && /text\/html|application\/xhtml\+xml/.test(contentType) 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 = Buffer.from('</head>')
const HEAD_UP = Buffer.from('</HEAD>') const HEAD_UP = Buffer.from('</HEAD>')
@ -25,16 +49,18 @@ const BODY = Buffer.from('</body>')
const BODY_UP = Buffer.from('</BODY>') const BODY_UP = Buffer.from('</BODY>')
function chunkByteReplace (_this, chunk, enc, callback, append) { function chunkByteReplace (_this, chunk, enc, callback, append) {
if (append && append.head) { if (append) {
const ret = injectScriptIntoHtml([HEAD, HEAD_UP], chunk, append.head) if (append.head) {
if (ret != null) { const ret = injectScriptIntoHtml([HEAD, HEAD_UP], chunk, append.head)
chunk = ret if (ret != null) {
chunk = ret
}
} }
} if (append.body) {
if (append && append.body) { const ret = injectScriptIntoHtml([BODY, BODY_UP], chunk, append.body)
const ret = injectScriptIntoHtml([BODY, BODY_UP], chunk, append.body) if (ret != null) {
if (ret != null) { chunk = ret
chunk = ret }
} }
} }
_this.push(chunk) _this.push(chunk)
@ -57,10 +83,41 @@ function injectScriptIntoHtml (tags, chunk, script) {
return null return null
} }
function handleResponseHeaders (res, proxyRes) {
Object.keys(proxyRes.headers).forEach(function (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].indexOf('self') < 0) {
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 contextPath = '/____ds_script____/'
const monkey = require('../../monkey') const monkey = require('../../monkey')
module.exports = { module.exports = {
requestIntercept (context, req, res, ssl, next) { requestIntercept (context, req, res, ssl, next) {
const { rOptions, log, setting } = context const { rOptions, log, setting } = context
if (rOptions.path.indexOf(contextPath) !== 0) { if (rOptions.path.indexOf(contextPath) !== 0) {
@ -87,7 +144,7 @@ module.exports = {
return true return true
}, },
responseInterceptor (req, res, proxyReq, proxyRes, ssl, next, append) { responseInterceptor (req, res, proxyReq, proxyRes, ssl, next, append) {
if (!append.head && !append.body) { if (append == null || (!append.head && !append.body)) {
next() next()
return return
} }
@ -99,50 +156,38 @@ module.exports = {
if (!isHtml || contentLengthIsZero) { if (!isHtml || contentLengthIsZero) {
next() next()
return return
} else {
Object.keys(proxyRes.headers).forEach(function (key) {
if (proxyRes.headers[key] !== undefined) {
// let newkey = key.replace(/^[a-z]|-[a-z]/g, (match) => {
// return match.toUpperCase()
// })
const newkey = key
if (isHtml && key === 'content-length') {
// do nothing
return
}
if (isHtml && 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].indexOf('self') < 0) {
policy = policy.replace('script-src', 'script-src \'self\' ')
}
}
res.setHeader(newkey, policy)
return
}
res.setHeader(newkey, proxyRes.headers[key])
}
})
res.writeHead(proxyRes.statusCode)
const isGzip = httpUtil.isGzip(proxyRes)
if (isGzip) {
proxyRes.pipe(new zlib.Gunzip())
.pipe(through(function (chunk, enc, callback) {
chunkByteReplace(this, chunk, enc, callback, append)
})).pipe(new zlib.Gzip()).pipe(res)
} else {
proxyRes.pipe(through(function (chunk, enc, callback) {
chunkByteReplace(this, chunk, enc, callback, append)
})).pipe(res)
}
} }
// 先处理头信息
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() next()
} },
httpUtil,
handleResponseHeaders
} }