如何使用 Node.js 通过代理发送 HTTP/2 请求?

Posted

技术标签:

【中文标题】如何使用 Node.js 通过代理发送 HTTP/2 请求?【英文标题】:How do you send HTTP/2 requests via a proxy using Node.js? 【发布时间】:2020-10-31 13:11:32 【问题描述】:

我想使用 Node.js 的 http2 库通过代理向服务器发送 HTTP/2 请求。

出于测试目的,我使用 Charles v4.2.7 作为代理,但 Charles 无法代理请求。 Charles 显示Malformed request URL "*" 错误,因为它收到的请求是PRI * HTTP/2.0(HTTP/2 Connection Preface)。我可以使用 cURL(例如curl --http2 -x localhost:8888 https://cypher.codes)通过我的 Charles 代理成功发送 HTTP/2 请求,所以我认为这不是 Charles 的问题,而是我的 Node.js 实现的问题。

这是我的 Node.js HTTP/2 客户端实现,它尝试通过我在 http://localhost:8888 上监听的 Charles 代理向 https://cypher.codes 发送 GET 请求:

const http2 = require('http2');

const client = http2.connect('http://localhost:8888');
client.on('error', (err) => console.error(err));

const req = client.request(
  ':scheme': 'https',
  ':method': 'GET',
  ':authority': 'cypher.codes',
  ':path': '/',
);
req.on('response', (headers, flags) => 
  for (const name in headers) 
    console.log(`$name: $headers[name]`);
  
);

req.setEncoding('utf8');
let data = '';
req.on('data', (chunk) =>  data += chunk; );
req.on('end', () => 
  console.log(`\n$data`);
  client.close();
);
req.end();

这是我在运行node proxy.js 时遇到的Node.js 错误(proxy.js 是包含上述代码的文件):

events.js:200
      throw er; // Unhandled 'error' event
      ^

Error [ERR_HTTP2_ERROR]: Protocol error
    at Http2Session.onSessionInternalError (internal/http2/core.js:746:26)
Emitted 'error' event on ClientHttp2Stream instance at:
    at emitErrorNT (internal/streams/destroy.js:92:8)
    at emitErrorAndCloseNT (internal/streams/destroy.js:60:3)
    at processTicksAndRejections (internal/process/task_queues.js:81:21) 
  code: 'ERR_HTTP2_ERROR',
  errno: -505

我使用详细输出重新运行了上述 cURL 请求,看起来 cURL 首先使用 HTTP/1 向代理发送 CONNECT,然后使用 HTTP/2 发送 GET 请求。

$ curl -v --http2 -x localhost:8888 https://cypher.codes         
*   Trying ::1... 
* TCP_NODELAY set
* Connected to localhost (::1) port 8888 (#0)
* allocate connect buffer!
* Establish HTTP proxy tunnel to cypher.codes:443
> CONNECT cypher.codes:443 HTTP/1.1  
> Host: cypher.codes:443
> User-Agent: curl/7.64.1                                                                                              
> Proxy-Connection: Keep-Alive
> 
< HTTP/1.1 200 Connection established
<               
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!                                                                                             
* ALPN, offering h2                                    
* ALPN, offering http/1.1                                
* successfully set certificate verify locations:                                                                       
*   CAfile: /etc/ssl/cert.pem    
  CApath: none                                                                                                         
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* CONNECT phase completed!
* CONNECT phase completed!   
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):                                                                                                                                                                                     
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): 
* TLSv1.2 (OUT), TLS handshake, Finished (20):                                                                                                                                                                                                
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2        
* Server certificate:                
*  subject: CN=cypher.codes
*  start date: Jun 21 04:38:35 2020 GMT
*  expire date: Sep 19 04:38:35 2020 GMT
*  subjectAltName: host "cypher.codes" matched cert's "cypher.codes"
*  issuer: CN=Charles Proxy CA (8 Oct 2018, mcypher-mbp.local); OU=https://charlesproxy.com/ssl; O=XK72 Ltd; L=Auckland; ST=Auckland; C=NZ
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7ff50d00d600)
> GET / HTTP/2
> Host: cypher.codes
> User-Agent: curl/7.64.1
> Accept: */*
> 
...

我想尝试通过 Node.js 做同样的事情(首先发送一个 HTTP/1 CONNECT 请求,然后在同一个 TCP 连接上发送我的 HTTP/2 请求),但我不知道该怎么做.创建 HTTP/2 客户端(即http2.connect('http://localhost:8888');)的行为会发送 HTTP/2 连接前言。我考虑过首先使用 HTTP/1 创建连接(例如使用 http 库),然后将其升级到 HTTP/2,但我找不到任何有关如何执行此操作的示例。

有人可以帮我使用 Node.js 通过代理发送 HTTP/2 请求吗?


更新(2020-07-13):我在首先使用 HTTP/1 创建连接、发送 CONNECT 请求,然后尝试使用 HTTP/2 发送 GET 请求方面取得了更多进展在同一个插座上。我可以看到在 Charles 中通过了 CONNECT 请求,但没有看到额外的 GET 请求,这表明我在尝试对 HTTP/2 请求使用相同的套接字时仍然做错了。这是我更新的代码:

const http = require('http');
const http2 = require('http2');

const options = 
  hostname: 'localhost',
  port: 8888,
  method: 'CONNECT',
  path: 'cypher.codes:80',
  headers: 
    Host: 'cypher.codes:80',
    'Proxy-Connection': 'Keep-Alive',
    'Connection': 'Keep-Alive',
  ,
;
const connReq = http.request(options);
connReq.end();

connReq.on('connect', (_, socket) => 
  const client = http2.connect('https://cypher.codes', 
    createConnection: () =>  return socket ,
  );
  client.on('connect', () => console.log('http2 client connect success'));
  client.on('error', (err) => console.error(`http2 client connect error: $err`));

  const req = client.request(
    ':path': '/',
  );
  req.setEncoding('utf8');
  req.on('response', (headers, flags) => 
    let data = '';
    req.on('data', (chunk) =>  data += chunk; );
    req.on('end', () => 
      console.log(data);
      client.close();
    );
  );
  req.end();
);

【问题讨论】:

【参考方案1】:

要通过不理解的代理来隧道 HTTP/2,您需要使用 HTTP/1.1 进行初始连接,然后仅在隧道中使用 HTTP/2。您的代码从一开始就使用 HTTP/2,这是行不通的。

要真正建立该隧道,您首先向目标主机发送 HTTP CONNECT 请求,并收到 200 响应,然后连接上的其他所有内容都将在您和目标主机之间来回转发。

一旦你的隧道工作了,你就可以发送 HTTP/2(或目标服务器可以理解的任何其他内容),它会直接到达你的目标。

在节点中执行此操作的代码如下所示:

const http = require('http');
const http2 = require('http2');

// Build a HTTP/1.1 CONNECT request for a tunnel:
const req = http.request(
  method: 'CONNECT',
  host: '127.0.0.1',
  port: 8888,
  path: 'cypher.codes'
);
req.end(); // Send it

req.on('connect', (res, socket) => 
  // When you get a successful response, use the tunnelled socket
  // to make your new request.
  const client = http2.connect('https://cypher.codes', 
    // Use your existing socket, wrapped with TLS for HTTPS:
    createConnection: () => tls.connect(
      socket: socket,
      ALPNProtocols: ['h2']
    )
  );

  // From here, use 'client' to do HTTP/2 as normal through the tunnel
);

我最近也一直在研究我自己的工具的内部结构,以添加对代理的完整 HTTP/2 支持,并将其编写为 over here,这可能对您非常重要。 https://github.com/httptoolkit/mockttp/blob/h2/test/integration/http2.spec.ts 中的测试有更多和更大的在节点中隧道 HTTP/2 的例子,所以这些也绝对值得一看。当然,这一切仍在开发中,所以如果您有任何问题或发现任何错误,请告诉我。

【讨论】:

感谢您的回答,我在使用远程代理(不是localhost)时遇到了问题,这里是问题:***.com/questions/65753359/…【参考方案2】:

@TimPerry 的回答几乎对我有用,但它错过了几件事:身份验证以及如何避免 TLS 证书错误。

所以这是我的更新版本:

const http = require('http');
const http2 = require('http2');
const tls = require('tls');

// Build a HTTP/1.1 CONNECT request for a tunnel:
const username = '...';
const password = '...';
const req = http.request(
  method: 'CONNECT',
  host: '127.0.0.1',
  port: 8888,
  path: 'website.com', //the destination domain
  headers:  //this is how we authorize the proxy, skip it if you don't need it
    'Proxy-Authorization': 'Basic ' + Buffer.from(username + ':' + password).toString('base64')
  
);
req.end(); // Send it

req.on('connect', (res, socket) => 
  // When you get a successful response, use the tunnelled socket to make your new request
  const client = http2.connect('https://website.com', 
    createConnection: () => tls.connect(
      host: 'website.com', //this is necessary to avoid certificate errors
      socket: socket,
      ALPNProtocols: ['h2']
    )
  );

  // From here, use 'client' to do HTTP/2 as normal through the tunnel
);

【讨论】:

以上是关于如何使用 Node.js 通过代理发送 HTTP/2 请求?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 node.js 中同时使用多个代理(代理)

通过http代理执行node js命令

如何让Elastic Beanstalk nginx支持的代理服务器从HTTP自动重定向到HTTPS?

带有自定义 Http(s) 代理和 Connect.js 中间件的 Node.js 代理

使用 node.js http2 模块的服务器发送事件 - 我应该如何将它与流 / pushStream 一起使用?

如何使用 node.js 发送 HTTP/2.0 请求