第四节:一个简易的HTTPS代理

结合前3节的内容,下面实现一个简易的HTTPS代理。

在第二节了解了一个HTTPS请求的代理过程,在建立链接的第一步是一个HTTP CONNECT请求,在这一步可以获得客户端请求目标网站的域名(这么说不是很准确,具体可看看SNI)。用预先安装好的CA证书和密钥,生成对应域名的子证书。这个过程其实就是一个HTTPS代理的核心步骤。

获取https所请求的域名

  1. const http = require('http');
  2. const url = require('url');
  3. const net = require('net');
  4. const createFakeHttpsWebSite = require('./createFakeHttpsWebSite')
  5. let httpTunnel = new http.Server();
  6. // 启动端口
  7. let port = 6789;
  8. httpTunnel.listen(port, () => {
  9. console.log(`简易HTTPS中间人代理启动成功,端口:${port}`);
  10. });
  11. httpTunnel.on('error', (e) => {
  12. if (e.code == 'EADDRINUSE') {
  13. console.error('HTTP中间人代理启动失败!!');
  14. console.error(`端口:${port},已被占用。`);
  15. } else {
  16. console.error(e);
  17. }
  18. });
  19. // https的请求通过http隧道方式转发
  20. httpTunnel.on('connect', (req, cltSocket, head) => {
  21. // connect to an origin server
  22. var srvUrl = url.parse(`http://${req.url}`);
  23. console.log(`CONNECT ${srvUrl.hostname}:${srvUrl.port}`);
  24. // 根据域名生成对应的https服务
  25. createFakeHttpsWebSite(srvUrl.hostname, (port) => {
  26. var srvSocket = net.connect(port, '127.0.0.1', () => {
  27. cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +
  28. 'Proxy-agent: MITM-proxy\r\n' +
  29. '\r\n');
  30. srvSocket.write(head);
  31. srvSocket.pipe(cltSocket);
  32. cltSocket.pipe(srvSocket);
  33. });
  34. srvSocket.on('error', (e) => {
  35. console.error(e);
  36. });
  37. })
  38. });

伪造一个https服务站点

  1. /**
  2. * 根据域名生成一个伪造的https服务
  3. * @param {[type]} domain [description]
  4. * @param {[type]} successFun [description]
  5. * @return {[type]} [description]
  6. */
  7. function createFakeHttpsWebSite(domain, successFun) {
  8. const fakeCertObj = createFakeCertificateByDomain(caKey, caCert, domain)
  9. var fakeServer = new https.Server({
  10. key: fakeCertObj.key,
  11. cert: fakeCertObj.cert,
  12. SNICallback: (hostname, done) => {
  13. let certObj = createFakeCertificateByDomain(caKey, caCert, hostname)
  14. done(null, tls.createSecureContext({
  15. key: pki.privateKeyToPem(certObj.key),
  16. cert: pki.certificateToPem(certObj.cert)
  17. }))
  18. }
  19. });
  20. fakeServer.listen(0, () => {
  21. var address = fakeServer.address();
  22. successFun(address.port);
  23. });
  24. fakeServer.on('request', (req, res) => {
  25. // 解析客户端请求
  26. var urlObject = url.parse(req.url);
  27. let options = {
  28. protocol: 'https:',
  29. hostname: req.headers.host.split(':')[0],
  30. method: req.method,
  31. port: req.headers.host.split(':')[1] || 80,
  32. path: urlObject.path,
  33. headers: req.headers
  34. };
  35. res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8'});
  36. res.write(`<html><body>我是伪造的: ${options.protocol}//${options.hostname} 站点</body></html>`)
  37. res.end();
  38. });
  39. fakeServer.on('error', (e) => {
  40. console.error(e);
  41. });
  42. }
  43. /**
  44. * 根据所给域名生成对应证书
  45. * @param {[type]} caKey [description]
  46. * @param {[type]} caCert [description]
  47. * @param {[type]} domain [description]
  48. * @return {[type]} [description]
  49. */
  50. function createFakeCertificateByDomain(caKey, caCert, domain) {
  51. var keys = pki.rsa.generateKeyPair(2046);
  52. var cert = pki.createCertificate();
  53. cert.publicKey = keys.publicKey;
  54. cert.serialNumber = (new Date()).getTime()+'';
  55. cert.validity.notBefore = new Date();
  56. cert.validity.notBefore.setFullYear(cert.validity.notBefore.getFullYear() - 1);
  57. cert.validity.notAfter = new Date();
  58. cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1);
  59. var attrs = [{
  60. name: 'commonName',
  61. value: domain
  62. }, {
  63. name: 'countryName',
  64. value: 'CN'
  65. }, {
  66. shortName: 'ST',
  67. value: 'GuangDong'
  68. }, {
  69. name: 'localityName',
  70. value: 'ShengZhen'
  71. }, {
  72. name: 'organizationName',
  73. value: 'https-mitm-proxy-handbook'
  74. }, {
  75. shortName: 'OU',
  76. value: 'https://github.com/wuchangming/https-mitm-proxy-handbook'
  77. }];
  78. cert.setIssuer(caCert.subject.attributes);
  79. cert.setSubject(attrs);
  80. cert.setExtensions([{
  81. name: 'basicConstraints',
  82. critical: true,
  83. cA: false
  84. },
  85. {
  86. name: 'keyUsage',
  87. critical: true,
  88. digitalSignature: true,
  89. contentCommitment: true,
  90. keyEncipherment: true,
  91. dataEncipherment: true,
  92. keyAgreement: true,
  93. keyCertSign: true,
  94. cRLSign: true,
  95. encipherOnly: true,
  96. decipherOnly: true
  97. },
  98. {
  99. name: 'subjectAltName',
  100. altNames: [{
  101. type: 2,
  102. value: domain
  103. }]
  104. },
  105. {
  106. name: 'subjectKeyIdentifier'
  107. },
  108. {
  109. name: 'extKeyUsage',
  110. serverAuth: true,
  111. clientAuth: true,
  112. codeSigning: true,
  113. emailProtection: true,
  114. timeStamping: true
  115. },
  116. {
  117. name:'authorityKeyIdentifier'
  118. }]);
  119. cert.sign(caKey, forge.md.sha256.create());
  120. return {
  121. key: keys.privateKey,
  122. cert: cert
  123. };
  124. }

完整源码:[../code/chapter4]

npm script运行方式

  1. npm run simpleHttpsProxy

这样一个简易的HTTPS代理就完成了。
第四节:一个简易的HTTPS代理 - 图1

第五节:总结