我创建了一个简单的HTTP代理,该代理使用HTTP CONNECT method执行HTTP隧道。
const http = require('http');
const https = require('https');
const pem = require('pem');
const net = require('net');
const util = require('util');
const createHttpsServer = (callback) => {
pem.createCertificate({
days: 365,
selfSigned: true
}, (error, {serviceKey, certificate, csr}) => {
const httpsOptions = {
ca: csr,
cert: certificate,
key: serviceKey
};
const server = https.createServer(httpsOptions, (req, res) => {
// How do I know I here whats the target server port?
res.writeHead(200);
res.end('OK');
});
server.listen((error) => {
if (error) {
console.error(error);
} else {
callback(null, server.address().port);
}
});
});
};
const createProxy = (httpsServerPort) => {
const proxy = http.createServer();
proxy.on('connect', (request, requestSocket, head) => {
// Here I know whats the target server PORT.
const targetServerPort = Number(request.url.split(':')[1]);
console.log('target server port', targetServerPort);
const serverSocket = net.connect(httpsServerPort, 'localhost', () => {
requestSocket.write(
'HTTP/1.1 200 Connection established\r\n\r\n'
);
serverSocket.write(head);
serverSocket.pipe(requestSocket);
requestSocket.pipe(serverSocket);
});
});
proxy.listen(9000);
};
const main = () => {
createHttpsServer((error, httpsServerPort) => {
if (error) {
console.error(error);
} else {
createProxy(httpsServerPort);
}
});
};
main();
服务器接受HTTPS连接,并以“确定”消息响应,而无需进一步转发请求。
您可以在代码中看到(请参阅// Here I know whats the target server PORT.
),我可以在HTTP CONNECT事件处理程序中获取目标服务器的端口。但是,我无法弄清楚如何将此信息传递到createHttpsServer
HTTP服务器路由器(请参阅// How do I know I here whats the target server port?
)。
在建立TLS连接时,如何传递其他信息?
可以通过运行以下代码来测试上述代码:
$ node proxy.js &
$ curl --proxy http://localhost:9000 https://localhost:59194/foo.html -k
目标是回答“ OK localhost:59194”。
答案 0 :(得分:2)
(感谢)您无法将任何内容添加到TLS流中,除非将其隧道传输到另一个协议中(这是Connect方法已经完成的工作)。但是,由于HTTP代理服务器和HTTPS服务器位于同一代码库中,因此您无需再次通过网络发送TLS流。相反,您想解析TLS流,然后可以将任何变量传递给处理它的代码。
但是,在解析TLS之后,您仍将拥有原始HTTP流,并且需要HTTP服务器将其转换为请求并处理响应。
一种快速而又肮脏的方法是使用Node的HTTPS服务器来解码TLS和解析HTTP。但是服务器的API并未提供处理已连接的套接字的功能,并且服务器的代码并未与连接代码完全分开。因此,您需要劫持服务器的内部连接处理逻辑-当然,在将来进行更改时,这很容易损坏:
const http = require('http');
const https = require('https');
const pem = require('pem');
const createProxy = (httpsOptions) => {
const proxy = http.createServer();
proxy.on('connect', (request, requestSocket, head) => {
const server = https.createServer(httpsOptions, (req, res) => {
res.writeHead(200);
res.end('OK');
});
server.emit('connection', requestSocket);
requestSocket.write('HTTP/1.1 200 Connection established\r\n\r\n');
});
proxy.listen(9000);
};
const main = () => {
pem.createCertificate({
days: 365,
selfSigned: true
}, (error, {serviceKey, certificate, csr}) => {
createProxy({
ca: csr,
cert: certificate,
key: serviceKey
});
});
};
main();
为避免在每个请求上创建HTTPS服务器实例,您可以将实例移出并将数据附加到套接字对象上:
const server = https.createServer(httpsOptions, (req, res) => {
res.writeHead(200);
// here we reach to the net.Socket instance saved on the tls.TLSSocket object,
// for extra dirtiness
res.end('OK ' + req.socket._parent.marker + '\n');
});
proxy.on('connect', (request, requestSocket, head) => {
requestSocket.marker = Math.random();
server.emit('connection', requestSocket);
requestSocket.write('HTTP/1.1 200 Connection established\r\n\r\n');
});
使用上面的代码,如果您连续执行几个请求:
curl --proxy http://localhost:9000 https://localhost:59194/foo.html \
https://localhost:59194/foo.html https://localhost:59194/foo.html \
https://localhost:59194/foo.html https://localhost:59194/foo.html -k
然后您还会注意到它们是在单个连接上处理的,这很不错:
OK 0.6113572936982015
OK 0.6113572936982015
OK 0.6113572936982015
OK 0.6113572936982015
OK 0.6113572936982015
我不太保证在代理服务器已经管理过套接字后将套接字交给HTTPS服务器不会破坏任何东西。 The server has the presence of mind to not overwrite another instance on the socket object,但在其他方面似乎与套接字紧密相关。您将需要使用运行时间更长的连接进行测试。
至于head
参数,which can indeed contain initial data:
requestSocket.unshift(head)
将其放回流中,但是我不确定代理服务器不会立即使用它。requestSocket.emit('data', head)
将其移至HTTPS服务器,因为the HTTP server seems to use the stream events,但是TLS socket source calls read()
for whatever reason,并且这与事件,所以我不确定它们之间如何协同工作。stream.Duplex
制作自己的包装程序,该包装程序将转发所有调用和事件,但如果存在此初始缓冲区,则read()
除外,然后使用该包装程序代替requestSocket
。但是随后您还需要复制“数据”事件,in accordance with the logic of Node's readable streams。head
并将其套接字传递给它,然后使用该流代替HTTPS服务器的套接字-不确定它是否将与HTTP服务器对套接字的霸道管理兼容。
一种更干净的方法是对TLS流进行解码,并对结果HTTP流使用独立的解析器。值得庆幸的是,Node有一个tls
模块,该模块很好地隔离了,并将TLS套接字转换为常规套接字:
proxy.on('connect', (request, requestSocket, head) => {
const httpSocket = new tls.TLSSocket(requestSocket, {
isServer: true,
// this var can be reused for all requests,
// as it's normally saved on an HTTPS server instance
secureContext: tls.createSecureContext(httpsOptions)
});
...
});
See caveats on tls.createSecureContext
关于复制HTTPS服务器的行为。
A,Node的HTTP解析器不是那么有用:它是一个C库,需要在套接字和解析器调用之间进行大量工作。与上面使用的HTTP服务器内部相比,该API可以(并且确实)在版本之间进行更改,而不会发出警告,并且具有更大的表面不兼容性。
有用于解析HTTP的NPM模块: one,two,但似乎没有一个过于成熟和维护。
我也对自定义HTTP服务器的可行性表示怀疑,因为由于极端情况,难以调试的超时问题等原因,网络套接字随着时间的流逝往往需要大量的培养,而这些问题都应该在解决方案中解决。节点的HTTP服务器。
P.S。一个可能的研究领域是集群模块如何处理连接:afaik集群中的父进程将连接套接字移交给子进程,但它不会派生到每个请求上-这表明子进程以某种方式处理连接的套接字,在HTTP服务器实例之外的代码中。但是,由于集群模块现在处于核心地位,因此它可能会利用非公共API。