簡體   English   中英

如何使用 Node.js 通過代理發送 HTTP/2 請求?

[英]How do you send HTTP/2 requests via a proxy using Node.js?

我想使用 Node.js 的http2庫通過代理向服務器發送 HTTP/2 請求。

我使用 Charles v4.2.7 作為代理,用於測試目的,但 Charles 無法代理該請求。 Charles 顯示Malformed request URL "*"錯誤,因為它收到的請求是PRI * HTTP/2.0HTTP/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
}

我用詳細的 output 重新運行了上面的 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 請求方面取得了更多進展。 我可以看到 CONNECT 請求在 Charles 中通過,但沒有看到額外的 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();
});

要通過不理解它的代理來隧道 HTTP/2,您需要使用 HTTP/1.1 進行初始連接,然后僅在隧道中使用 HTTP/2。 您的代碼從一開始就使用 HTTP/2,這是行不通的。

要實際建立該隧道,您首先向目標主機發送 HTTP CONNECT 請求,並收到 200 響應,然后將來連接上的所有其他內容都會在您和目標主機之間來回轉發。

一旦你的隧道工作,你可以發送 HTTP/2(或目標服務器理解的任何其他內容),它將 go 直接發送到你的目標。

在節點中執行此操作的代碼如下所示:

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 示例,所以這些絕對值得也看看。 當然,這一切仍在開發中,所以如果您有任何問題或發現任何錯誤,請告訴我。

我在@Stalinko 的回答中收到“ERR_HTTP2_ERROR”和“ERR_HTTP2_PROTOCOL_ERROR”錯誤,所以我需要找到一個替代方案......

為了顯示我的解決方案,我們將向 API 發出請求,將您的 IP 返回為 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 套接字,而是在 HTTP/2.0 客戶端中注入了我的 TCP 套接字。

方法文檔中未明確列出套接字選項,但該方法接受所有net.connect()tls.connect()選項。

您可以在此處找到有關http2.connect方法的所有文檔: HTTP 2 Node JS 文檔

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM