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

4
我希望使用Node.js的 http2 库通过代理向服务器发送HTTP/2请求。为了测试,我使用Charles v4.2.7作为代理,但是Charles无法代理该请求。Charle显示Malformed request URL "*"错误,因为它接收到的请求是 PRI * HTTP/2.0(HTTP/2连接前言)。 我可以通过cURL成功地通过我的Charles代理发送HTTP/2请求(例如:curl --http2 -x localhost:8888 https://cypher.codes),因此我认为这不是与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();
});
3个回答

8
为了通过不支持HTTP/2的代理服务器建立HTTP/2隧道,您需要在起始连接中使用HTTP/1.1,然后仅在隧道中使用HTTP/2。您的代码从一开始就使用HTTP/2,这是行不通的。
实际上,要建立该隧道,您首先需要发送一个针对目标主机的HTTP CONNECT请求,并接收一个200响应,然后以后连接中的所有其他内容都将在您和目标主机之间相互转发。
一旦您建立了该隧道,您可以发送HTTP/2(或任何目标服务器理解的其他内容),它将直接到达您的目标。
在Node.js中执行此操作的代码如下:
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代理支持,并在这里记录了下来,对你来说可能非常相关。在https://github.com/httptoolkit/mockttp/blob/h2/test/integration/http2.spec.ts上的测试包含更多和更大的示例,展示了如何在类似于节点的环境中进行HTTP/2隧道代理,这些值得一看。当然这些都还在开发阶段,如果你有任何问题或发现任何错误,请告诉我。


谢谢您的回答,我在使用远程代理(而不是本地主机)时遇到了问题,这是我的问题链接:https://stackoverflow.com/questions/65753359/node-http2-request-with-remote-proxy - ajimix

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
});

1

我在使用@Stalinko的答案时遇到了"ERR_HTTP2_ERROR"和"ERR_HTTP2_PROTOCOL_ERROR"错误,因此我需要寻找替代方案...

为了展示我的解决方案,我们将向一个返回你的IP地址的API发出请求,并以JSON格式返回结果,然后您可以根据自己的需求进行调整。

以下是代码:

/**
 * A URL without the path.
 */
const TARGET_AUTHOTIRY = 'https://api4.my-ip.io'

/**
 * You should use the host with the port equivalent to the protocol
 * HTTP => 80
 * HTTPS => 443
 */
const TARGET_HOST = 'api4.my-ip.io:443'

/**
 * Proxy configuration
 */
const PROXY_HOST = '<your_proxy_host>'
const PROXY_PORT = '<your_proxy_port>'
const PROXY_USERNAME = '<your_proxy_username>'
const PROXY_PASSWORD = '<your_proxy_password>'

/**
 * Establishes an connection to the target server throught the HTTP/1.0
 * proxy server.
 *
 * The CONNECT method tells the PROXY server where this connection should arive.
 *
 * After the connection is established you will be able to use the TCP socket to send data
 * to the TARGET server.
 */
const request = http.request({
  method: 'CONNECT',
  host: PROXY_HOST,
  port: PROXY_PORT,
  path: TARGET_HOST,
  headers: {
    'Host': TARGET_HOST,
    'Proxy-Authorization': `Basic ${Buffer.from(`${PROXY_USERNAME}:${PROXY_PASSWORD}`).toString('base64')}`
  }
})

/**
 * Wait the "connect" event and then uses the TCP socket to proxy the HTTP/2.0 connection throught.
 */
request.on('connect', (res, socket) => {
  /**
   * Check if it has successfully connected to the server
   */
  if (res.statusCode !== 200)
    throw new Error('Connection rejected by the proxy')

  /**
   * Use the TCP socket from the HTTP/1.0 as the socket for this new connection
   * without the need to establish the TLS connection manually and handle the errors
   * manually too.
   *
   * This method accepts all TCP and TLS options.
   */
  const client = http2.connect(TARGET_AUTHOTIRY, { socket })

  client.on('connect', () => {
    console.log('Connected to the page!')
  })

  /**
   * Request to check your IP
   */
  const req = client.request({
    ':path': '/ip.json',
  })

  req.on('response', (headers) => {
    console.log('Recieved a response')
  })

  /**
   * Stores the data recieved as a response
   */
  const buffers = []
  req.on('data', (buffer) => {
    buffers.push(buffer)
  })

  req.on('end', () => {
    console.log(Buffer.concat(buffers).toString('utf-8'))

    // Closes the connection with the server
    client.close()
  })

  req.end()
})

request.end()

我不是创建一个 TLS Socket,而是将 TCP Socket 注入 HTTP/2.0 客户端。

socket 选项在方法文档中没有明确列出,但该方法接受所有 net.connect()tls.connect() 选项。

你可以在这里找到有关 http2.connect 方法的所有文档: HTTP 2 Node JS 文档


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接