在 node.js 中实现一个通用代理中间件

marvin

如果把我们想要访问的真实 ip 称作最终客户, 那么 proxy 就可以称为代理人, 代理主要起以下几个作用:
控制对资源的访问
缓存
保护 api key
那么一个代理需要实现什么样的功能? 基本的需求是能够将被代理人的数据传输到最终客户并将最终客户的响应数据返回给被代理人,其他的需求包括自定义代理地址, 自定义请求头, 错误处理, 响应拦截, 请求拦截, 下面就来看看如何实现这样一个 proxy
解析请求参数
我们需要获取到请求参数, 然后构建一个向外部请求的参数:
const requestToFulfil = url.parse(clientRequest.url)
const options = {
method: clientRequest.method,
protocol: requestToFulfil.protocol,
auth: requestToFulfil.auth,
host: requestToFulfil.host,
port: requestToFulfil.port || 80,
hostname: requestToFulfil.hostname,
path: requestToFulfil.path,
headers: { ...clientRequest.headers, ...headers },
}
if(curl) {
const customeRequestToFulfil = url.parse(curl)
Object.assign(options, {
protocol: customeRequestToFulfil.protocol,
auth: customeRequestToFulfil.auth,
host: customeRequestToFulfil.host,
port: customeRequestToFulfil.port,
hostname: customeRequestToFulfil.hostname,
})
}
这里的 headers 即自定义请求头, 通过合并原始请求的请求头, 获得外部请求头, curl 为自定义代理地址, 如果存在则需要重新设置请求的协议, auth 认证, host 主机, port 端口, hostname 主机名的值为代理地址对应的值
拦截请求
如果某些请求路径不能被放行, 则可以通过配置一系列拦截函数对请求进行过滤, 过滤函器定义如下:
const blockResources = (options, ...assetMethods) => {
return [...assetMethods].some(assetCheck => assetCheck(options.path))
}
// ...
if(blockResources(options, ...blockers)) {
return clientResponse.end()
}
// ...
如果有一个过滤器命中 some 函数返回 true, 则直接结束请求, 返回一个空响应
发送代理请求
对于通过拦截器的请求, 接下来就准备向外部转发, 首先需要定义一个执行请求函数:
const executeRequest = (options, clientRequest, clientResponse, { onError, onHeader }) => {
const externalRequest = http.request(options, externalResponse => {
if(!onHeader(externalResponse.headers, clientRequest, clientResponse)) {
return;
}
clientResponse.writeHead(externalResponse.statusCode, externalResponse.headers)
externalResponse.on('data', chunk => {
clientResponse.write(chunk)
})
externalResponse.on('end', () => {
clientResponse.end()
})
})
clientRequest.on('data', chunk => {
externalRequest.write(chunk)
})
clientRequest.on('end', () => {
externalRequest.end()
})
externalRequest.on('error', (e) => {
onError(e, clientRequest, clientResponse)
})
}
// ...
executeRequest(options, clientRequest, clientResponse, { onError, onHeader })
// ...
首先通过 options 发送外部请求,在原始请求发送数据的时候将这些数据发送给外部请求, 当外部请求发生错误将错误信息传给错误处理函数, 最后在外部请求响应的时候,通过自定义的 onHeader 响应拦截函数判断是否需要继续处理, 当需要继续处理的时候我们将外部响应的请求头写回原始响应对象, 并在外部请求发送数据的时候将这些数据也写回给被代理人, 结束的时候关闭响应
写在最后
代理实际上就是一个信息中转站, 通过 node 的 stream 机制能够以简洁的方式实现
本文完整代码
const http = require('http');
const url = require('url');
const blockResources = (options, ...assetMethods) => {
return [...assetMethods].some(assetCheck => assetCheck(options.path))
}
const executeRequest = (options, clientRequest, clientResponse, { onError, onHeader }) => {
const externalRequest = http.request(options, externalResponse => {
if(!onHeader(externalResponse.headers, clientRequest, clientResponse)) {
return;
}
clientResponse.writeHead(externalResponse.statusCode, externalResponse.headers)
externalResponse.on('data', chunk => {
clientResponse.write(chunk)
})
externalResponse.on('end', () => {
clientResponse.end()
})
})
clientRequest.on('data', chunk => {
externalRequest.write(chunk)
})
clientRequest.on('end', () => {
externalRequest.end()
})
externalRequest.on('error', (e) => {
onError(e, clientRequest, clientResponse)
})
}
const proxy = (clientRequest, clientResponse, {
url: curl,
headers,
onError,
onHeader,
blockers
}) => {
const requestToFulfil = url.parse(clientRequest.url)
const options = {
method: clientRequest.method,
protocol: requestToFulfil.protocol,
auth: requestToFulfil.auth,
host: requestToFulfil.host,
port: requestToFulfil.port || 80,
hostname: requestToFulfil.hostname,
path: requestToFulfil.path,
headers: { ...clientRequest.headers, ...headers },
}
if(curl) {
const customeRequestToFulfil = url.parse(curl)
Object.assign(options, {
protocol: customeRequestToFulfil.protocol,
auth: customeRequestToFulfil.auth,
host: customeRequestToFulfil.host,
port: customeRequestToFulfil.port,
hostname: customeRequestToFulfil.hostname,
})
}
if(blockResources(options, ...blockers)) {
return clientResponse.end()
}
executeRequest(options, clientRequest, clientResponse, { onError, onHeader })
}
const proxyer = ({
url = '',
headers = {},
onError = () => {},
onHeader = () => {},
blockers = []
}) => {
return (req, res, next) => {
try {
proxy(req, res, {
url,
headers,
onError,
onHeader,
blockers
})
} catch(e) {
next(e)
}
}
}
export default proxyer;