node-http-proxy 源码解读

1 概述

node-http-proxy 模块用于转发 http 请求,其实现的大致原理为使用 http 或 https 模块搭建 node 代理服务器,将客户端发送的请求数据转发到目标服务器,再将响应输送到客户端。

2 实现

2.1 整体流程

同 koa 的中间件机制相仿,node-http-proxy 模块内部组装任务队列,在请求转发的过程中,将任务队列中的处理函数逐个执行。处理函数的意义通常是封装消息头,当然,最后一个处理函数用于转发请求、输出响应。

同常见的 ajax 模块,node-http-proxy 模块接受全局配置的 options,同时,在某个具体的请求中,又接受特定的配置项 opts。而客户端发送的请求可能是 http, https 请求,也可能是 websocket 请求,node-http-proxy 模块必须实现对这两类请求的不同处理。

上述三点,实际的代码体现在 createRightProxy 高阶函数中:

// 参数 type 用于区分请求类型,web 为普通 http, https 请求,ws 为 websocket 请求 function createRightProxy(type) { // 参数 options 为全局配置项 return function(options) { return function(req, res /*, [head], [opts] */) { // passes 任务队列 var passes = (type === ws) ? this.wsPasses : this.webPasses, args = [].slice.call(arguments), cntr = args.length - 1, head, cbl; // 解析回调函数 if(typeof args[cntr] === function) { cbl = args[cntr]; cntr--; } // 混入该请求中特定的配置项 opts var requestOptions = options; if( !(args[cntr] instanceof Buffer) && args[cntr] !== res ) { requestOptions = extend({}, options); extend(requestOptions, args[cntr]); cntr--; } // head if(args[cntr] instanceof Buffer) { head = args[cntr]; } // 请求的目标地址 [target, forward].forEach(function(e) { if (typeof requestOptions[e] === string) requestOptions[e] = parse_url(requestOptions[e]); }); if (!requestOptions.target && !requestOptions.forward) { return this.emit(error, new Error(Must provide a proper URL as target)); } // 挨个执行任务队列,处理消息头,转发请求 for(var i=0; i < passes.length; i++) { if(passes[i](req, res, requestOptions, head, this, cbl)) { break; } } }; }; }

因此,createRightProxy(web)(options), createRightProxy(ws)(options) 就能用于创建实际的请求转发函数。在 node-http-proxy 模块中,这两个函数分别表现为 ProxyServer 实例的 web, ws 方法。其中,proxyServer.web 方法作为 http 或 https 服务器 listen 方法的回调函数,http://proxyServer.ws 方法作为 upgrade 事件的绑定函数,从而能对接上客户端 ajax 请求、websocket 请求的执行时机。

function ProxyServer(options) { // ... // 创建转发 http, https; websocket 请求的处理函数 this.web = this.proxyRequest = createRightProxy(web)(options); this.ws= this.proxyWebsocketRequest= createRightProxy(ws)(options); // 任务队列,用于处理消息头、转发请求 this.webPasses = Object.keys(web).map(function(pass) {// this.web 方法执行过程中调用的任务队列 return web[pass]; }); this.wsPasses = Object.keys(ws).map(function(pass) {// this.ws 方法执行过程中调用的任务队列 return ws[pass]; }); // ... } ProxyServer.prototype.listen = function(port, hostname) { var self= this, closure = function(req, res) { self.web(req, res); };// 转发 http, https 请求 this._server= this.options.ssl ? https.createServer(this.options.ssl, closure) : http.createServer(closure); // 转发 websocket 请求 if(this.options.ws) { this._server.on(upgrade, function(req, socket, head) { self.ws(req, socket, head); }); } this._server.listen(port, hostname); return this; };

以上不涉及任务队列的具体实现,却构成了 node-http-proxy 模块整体处理流程。除外而外,ProxyServer 还提供 before(type, passName, callback), after(type, passName, callback) 原型方法,用于在任务队列的某个具体处理函数之前或之后插入一个处理函数 callback。

2.2 http, https 请求

this.webPasses 任务队列包含如下四种处理函数:deleteLength, timeout, XHeaders, stream。

deleteLength 函数:针对 DELETE 或 OPTIONS,且 headers[content-length] 未设置的情形,将 headers[content-length] 置为 0,并删除 headers[transfer-encoding] 消息头。timeout 函数:若设置了 options.timeout,调用 req.socket.setTimeout(options.timeout) 设置超时时间。XHeaders 函数:设置 x-forwarded-for, x-forwarded-port, x-forwarded-proto, x-forwarded-host 消息头,包含客户端和代理服务器的地址、端口、协议等内容(以 , 拼接 req.headers 同名属性即客户端内容、和代理服务器内容)。其中,x-forwarded-host 消息头只包含 req.headers.host,即代理服务器的主机名。由配置项 options.xfwd 启用 x-forwarded-* 消息头的设置。stream 函数:实际转发请求的处理函数。下文将作详解。

stream 函数的处理流程为:

调用 common.setupOutgoing 方法生成代理请求的配置项。通过 options.forward, options.target 区分 forward 和 target 两种模式。在 forward 模式下,只通过代理请求转发到目标服务器,输送给客户端的仍是代理服务器的响应。target 模式下,不只可以处理目标服务器的响应,且可监听许多事件对代理请求等作出处理。forward 和 target 模式可以并行存在,即同时指定 options.forward, options.target。

首先,common.setupOutgoing 的实现如下:

/** * 生成代理请求的配置项,将作为 http.request 的参数 * @param {object} outgoing 即 options.ssl 或 {} * @param {object} options 即 options * @param {object} req 即实际的请求 * @param {string|} forward 用于区分 forward 和 target 模式。值为 forward 或 */ common.setupOutgoing = function(outgoing, options, req, forward) { outgoing.port = options[forward || target].port || (isSSL.test(options[forward || target].protocol) ? 443 : 80); // #http_http_request_options_callback // host, host: 目标服务器的域名或 IP 地址 // socketPath: Unix 域 Socket(使用 host:port 或 socketPath) // ca: ca 证书 [host, hostname, socketPath, pfx, key, passphrase, cert, ca, ciphers, secureProtocol].forEach( function(e) { outgoing[e] = options[forward || target][e]; } ); // 请求方法 outgoing.method = options.method || req.method; // 请求头 outgoing.headers = extend({}, req.headers); if (options.headers){ extend(outgoing.headers, options.headers); } // 基本身份验证,如 user:password 用来计算 Authorization 请求头 if (options.auth) { outgoing.auth = options.auth; } if (options.ca) { outgoing.ca = options.ca; } if (isSSL.test(options[forward || target].protocol)) { outgoing.rejectUnauthorized = (typeof options.secure === "") ? true : options.secure; } // #http_new_agent_options // 长连接时设置 options.agent = { keepAlive, keepAliveMsecs } outgoing.agent = options.agent || false; outgoing.localAddress = options.localAddress; // 不是长连接,设置 outgoing.headers.connection 请求头 if (!outgoing.agent) { outgoing.headers = outgoing.headers || {}; if (typeof outgoing.headers.connection !== string || !upgradeHeader.test(outgoing.headers.connection) ) { outgoing.headers.connection = close; } } // 最终的请求路径由 options[forward|target], req.url 拼接产生,可根据 options 配置设定某项是否启用 var target = options[forward || target]; var targetPath = target && options.prependPath !== false ? (target.path || ) : ; var outgoingPath = !options.toProxy ? (url.parse(req.url).path || ) : req.url; outgoingPath = !options.ignorePath ? outgoingPath : ; outgoing.path = common.urlJoin(targetPath, outgoingPath); if (options.changeOrigin) { outgoing.headers.host = // 通过 requires-port 模块校验在使用某种协议的情况下,是否需要在 url 上拼接端口号 required(outgoing.port, options[forward || target].protocol) && !hasPort(outgoing.host) ? outgoing.host + : + outgoing.port : outgoing.host; } return outgoing; };

其次,stream 的实现如下:

调用 [http|https].request(outgoing) 创建代理请求。outgoing 由 common.setupOutgoing 函数获得。调用 (options.buffer || req).pipe(forwardReq) 方法转发代理请求。target 模式下,调用 web-outgoing 模块中的函数处理代理响应。function stream(req, res, options, _, server, clb) { server.emit(start, req, res, options.target || options.forward); // options.followRedirects 是否使用 follow-redirects 重定向 var agents = options.followRedirects ? followRedirects : nativeAgents; var http = agents.http; var https = agents.https; if(options.forward) { // 生成代理请求 var forwardReq = (options.forward.protocol === https: ? https : http).request( common.setupOutgoing(options.ssl || {}, options, req, forward) ); var forwardError = createErrorHandler(forwardReq, options.forward); req.on(error, forwardError); forwardReq.on(error, forwardError); // 转发代理请求 (options.buffer || req).pipe(forwardReq); // 非 target 模式,返回响应 if(!options.target) { return res.end(); } } // 生成代理请求 var proxyReq = (options.target.protocol === https: ? https : http).request( common.setupOutgoing(options.ssl || {}, options, req) ); proxyReq.on(socket, function(socket) { if(server) { server.emit(proxyReq, proxyReq, req, res, options); } }); if(options.proxyTimeout) { proxyReq.setTimeout(options.proxyTimeout, function() { proxyReq.abort(); }); } req.on(aborted, function () { proxyReq.abort(); }); var proxyError = createErrorHandler(proxyReq, options.target); req.on(error, proxyError); proxyReq.on(error, proxyError); function createErrorHandler(proxyReq, url) { return function proxyError(err) { if (req.socket.destroyed && err.code === ECONNRESET) { server.emit(econnreset, err, req, res, url); return proxyReq.abort(); } if (clb) { clb(err, req, res, url); } else { server.emit(error, err, req, res, url); } } } // 转发代理请求 (options.buffer || req).pipe(proxyReq); proxyReq.on(response, function(proxyRes) { if(server) { server.emit(proxyRes, proxyRes, req, res); } // 调用 web-outgoing 模块中的函数处理代理响应 if(!res.headersSent && !options.selfHandleResponse) { for(var i=0; i < web_o.length; i++) { if(web_o[i](req, res, proxyRes, options)) { break; } } } // #http_response_finished if (!res.finished) { // 通过事件处理代理响应 proxyRes.on(end, function () { if (server) server.emit(end, req, res, proxyRes); }); // 由 node-http-proxy 模块处理代理响应的情境下,返回响应 if (!options.selfHandleResponse) proxyRes.pipe(res); } else { if (server) server.emit(end, req, res, proxyRes); } }); }

最后,再来看一下 web-outgoing 模块对代理响应的处理(实现查看源码):

removeChunked 函数:当使用 http/1.0 时(通过 req.httpVersion === 1.0 判断,客户端决定),移除代理响应的 headers[transfer-encoding]。setConnection 函数:当使用 http/1.0 时,代理响应的 headers.connection 设为 req.headers.connection || close;当使用非 http/2.0 时,且 proxyRes.headers.connection 为否,将 代理响应的 headers.connection 设为 req.headers.connection || keep-alive。setRedirectHostRewrite 函数:根据 options.hostRewrite 或 options.autoRewrite 或 options.protocolRewrite 重写重定向地址 proxyRes.headers.location。代理相应的状态码须匹配 /^201|30(1|2|7|8)$/ 正则,且 proxyRes.headers.location 须与目标服务器同域名。writeHeaders 函数:将代理响应的消息头写入实际响应 res 的消息头中。下文将作详解。writeStatusCode 函数:将代理响应的 statusCode, statusMessage 写入返回给客户端的响应 res 中。

setRedirectHostRewrite 函数的代码实现:

function writeHeaders(req, res, proxyRes, options) { var rewriteCookieDomainConfig = options.cookieDomainRewrite, rewriteCookiePathConfig = options.cookiePathRewrite, preserveHeaderKeyCase = options.preserveHeaderKeyCase, rawHeaderKeyMap, setHeader = function(key, header) { if (header == ) return; if (rewriteCookieDomainConfig && key.toLowerCase() === set-cookie) { header = common.rewriteCookieProperty(header, rewriteCookieDomainConfig, domain); } if (rewriteCookiePathConfig && key.toLowerCase() === set-cookie) { header = common.rewriteCookieProperty(header, rewriteCookiePathConfig, path); } res.setHeader(String(key).trim(), header); }; if (typeof rewriteCookieDomainConfig === string) { //also test for rewriteCookieDomainConfig = { *: rewriteCookieDomainConfig }; } if (typeof rewriteCookiePathConfig === string) { //also test for rewriteCookiePathConfig = { *: rewriteCookiePathConfig }; } // #http_message_rawheaders if (preserveHeaderKeyCase && proxyRes.rawHeaders != ) { rawHeaderKeyMap = {}; for (var i = 0; i < proxyRes.rawHeaders.length; i += 2) { var key = proxyRes.rawHeaders[i]; rawHeaderKeyMap[key.toLowerCase()] = key; } } Object.keys(proxyRes.headers).forEach(function(key) { var header = proxyRes.headers[key]; if (preserveHeaderKeyCase && rawHeaderKeyMap) { key = rawHeaderKeyMap[key] || key; } setHeader(key, header); }); } // 根据 options.cookieDomainRewrite, options.cookiePathRewrite 重写 res.headers.cookie 中的 domain, path 属性 // cookie.domain 表示 cookie 所在的域 // cookie.path 表示 cookie 所在的目录 // 参考 [理解cookie的path和domain属性]( common.rewriteCookieProperty = function rewriteCookieProperty(header, config, property) { if (Array.isArray(header)) { return header.map(function (headerElement) { return rewriteCookieProperty(headerElement, config, property); }); } return header.replace(new RegExp("(;\\s*" + property + "=)([^;]+)", i), function(match, prefix, previousValue) { var newValue; if (previousValue in config) { newValue = config[previousValue]; } else if (* in config) { newValue = config[*]; } else { return match; } if (newValue) { return prefix + newValue; } else { return ; } }); };

2.3 websocket 请求

this.wsPasses 任务队列包含如下四种处理函数:checkMethodAndHeader, XHeaders, stream。

checkMethodAndHeader 函数:websocket 请求的请求方式必须是 get,且 headers.upgrade 请求头必须是 websocket,checkMethodAndHeader 函数用于校验请求方式和 headers.upgrade 请求头。XHeaders 函数:设置 x-forwarded-for, x-forwarded-port, x-forwarded-proto 消息头,包含客户端和代理服务器的地址、端口、协议等内容(以 , 拼接 req.headers 同名属性即客户端内容、和代理服务器内容)。由配置项 options.xfwd 启用 x-forwarded-* 消息头的设置。stream 函数:实际转发请求的处理函数。下文将作详解。

stream 函数的处理流程为:

调用 common.setupOutgoing 方法生成代理请求的配置项。调用 [http|https].request(outgoing) 创建代理请求 proxyReq。调用 proxyReq.end 发送代理请求。 监听 response 事件,修改消息头后将响应发送给客户端。监听 upgrade 事件,更换协议后,调用 proxySocket.pipe(socket).pipe(proxySocket) 再次发送代理请求。common.setupSocket = function(socket) { socket.setTimeout(0); socket.setNoDelay(true); socket.setKeepAlive(true, 0); return socket; }; function stream(req, socket, options, head, server, clb) { // 添加请求头内容 var createHttpHeader = function(line, headers) { return Object.keys(headers).reduce(function (head, key) { var value = headers[key]; if (!Array.isArray(value)) { head.push(key + : + value); return head; } for (var i = 0; i < value.length; i++) { head.push(key + : + value[i]); } return head; }, [line]) .join(\r\n) + \r\n\r\n; } common.setupSocket(socket); if (head && head.length) socket.unshift(head); // 创建代理请求 var proxyReq = (common.isSSL.test(options.target.protocol) ? https : http).request( common.setupOutgoing(options.ssl || {}, options, req) ); if (server) { server.emit(proxyReqWs, proxyReq, req, socket, options, head); } proxyReq.on(error, onOutgoingError); proxyReq.on(response, function (res) { // 属性响应到客户端 if (!res.upgrade) { socket.write(createHttpHeader(HTTP/ + res.httpVersion + + res.statusCode + + res.statusMessage, res.headers)); res.pipe(socket); } }); proxyReq.on(upgrade, function(proxyRes, proxySocket, proxyHead) { proxySocket.on(error, onOutgoingError); proxySocket.on(end, function () { server.emit(close, proxyRes, proxySocket, proxyHead); }); socket.on(error, function () { proxySocket.end(); }); common.setupSocket(proxySocket); if (proxyHead && proxyHead.length) proxySocket.unshift(proxyHead); socket.write(createHttpHeader(HTTP/1.1 101 Switching Protocols, proxyRes.headers)); proxySocket.pipe(socket).pipe(proxySocket);// 再次发送代理请求? server.emit(open, proxySocket); server.emit(proxySocket, proxySocket); }); return proxyReq.end(); // 发送代理请求 function onOutgoingError(err) { if (clb) { clb(err, req, socket); } else { server.emit(error, err, req, socket); } socket.end(); } }

3 应用

3.1 http-proxy-middleware

参见 http-proxy-middleware 源码解读。

3.1 nokit-filter-proxy

nokit-filter-proxy 库用于为 nokit 服务器添加代理功能。鉴于前端构建工具 dawn 使用 nokit 搭建本地调试服务器,nokit-filter-proxy 库也用于为 dn-middleware-server 中间件实现代理功能。同 http-proxy-middleware 库,nokit-filter-proxy 借助 node-http-proxy 实现服务器代理的都是先校验请求路径是否匹配转发策略,拦截并转发请求。nokit-filter-proxy 通过绑定 onRequest 事件函数,实现请求的拦截和转发。详见源码:

var httpProxy = require("http-proxy"); function ProxyFilter(server) { var self = this; var utils = self.utils = server.require("$./core/utils"); // proxy 配置,作为请求路径转发规则 self.configs = server.configs.proxy || {}; // 作为代理服务器的配置项 self.configs.options = self.configs.options || {}; // 代理请求设置 x-forwarded-for, x-forwarded-port, x-forwarded-proto, x-forwarded-host 消息头 if (utils.isNull(self.configs.options.xfwd)) { self.configs.options.xfwd = true; } // 代理请求设置 headers.host 消息头 if (utils.isNull(self.configs.options.changeOrigin)) { self.configs.options.changeOrigin = true; } // 请求路径转发规则,key - value 形式,key 为客户端请求路径正则,value 为目标服务器路径 self.configs.rules = self.configs.rules || {}; // 创建代理服务器 self.proxy = httpProxy.createProxyServer(self.configs.options); // 转发代理请求前,使用 headers 配置修改代理请求的消息头 self.onProxyReqHandler = self.onProxyReqHandler.bind(self); self.proxy.on("proxyReq", self.onProxyReqHandler); }; ProxyFilter.prototype.onProxyReqHandler = function(proxyReq, req, res, options) { var self = this; if (!self.configs.headers) return; self.utils.each(self.configs.headers, function(name, value) { proxyReq.setHeader(name, value); }); }; // self.matchRule 根据 rules 配置,解析出请求路径转发规则 ProxyFilter.prototype.matchRule = function(url) { var self = this; var rule = null; self.utils.each(self.configs.rules, function(exprText, target) { var expr = new RegExp(exprText); if (expr.test(url)) { var urlParts = expr.exec(url); rule = { url: urlParts.length > 1 ? urlParts[1] : url, target: target }; } }); return rule; }; ProxyFilter.prototype.onRequest = function(context, next) { var self = this; var res = context.res, req = context.req; var rule = self.matchRule(req.url); if (!rule) return next(); req.url = rule.url || "/"; // 转发代理请求 self.proxy.web(req, res, { "target": rule.target }); };

4 后记

这两篇文章都是在笔者整理完 proxy 设计模式后整理的。鉴于本人水平有限,文章难免错谬,仍望读者不吝赐教。