Как вы отправляете запросы HTTP / 2 через прокси, используя Node.js? - PullRequest
1 голос
/ 10 июля 2020

Я хотел бы отправлять запросы HTTP / 2 на сервер через прокси, используя Node.js http2 библиотеку.

Я использую Charles v4.2.7 в качестве прокси, в целях тестирования, но Чарльз не может проксировать запрос. Charles показывает ошибки Malformed request URL "*", поскольку полученный запрос - PRI * HTTP/2.0 ( HTTP / 2 Connection Preface ). Я могу успешно отправлять запросы HTTP / 2 через прокси Charles с помощью cURL (например, curl --http2 -x localhost:8888 https://cypher.codes), поэтому я не думаю, что это проблема Charles, а проблема с моей реализацией Node.js.

Вот моя реализация клиента Node.js HTTP / 2, которая пытается отправить запрос GET на https://cypher.codes через мой прокси Charles, прослушивающий http://localhost: 8888:

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.js, которую я получаю при запуске node proxy.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 сначала отправляет a CONNECT к прокси-серверу с использованием HTTP / 1 перед отправкой запроса GET с использованием HTTP / 2.

$ 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 запрос, а затем отправив мой запрос HTTP / 2 по тому же TCP-соединению), но я не уверен, как это сделать. Сам процесс создания клиента HTTP / 2 (например, http2.connect('http://localhost:8888');) отправляет предисловие к соединению HTTP / 2. Я подумал о том, чтобы сначала создать соединение с использованием HTTP / 1 (например, используя библиотеку http), а затем обновить его до HTTP / 2, но я не смог найти никаких примеров того, как это сделать.

Может кто-нибудь поможет мне отправить запрос HTTP / 2 через прокси, используя Node.js?

Обновление (2020-07-13): Я добился большего прогресса в том, чтобы сначала создать соединение с использованием HTTP / 1, отправить запрос CONNECT, а затем попытаться отправить запрос GET с использованием HTTP / 2 через та же розетка. Я вижу, что запрос 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();
});

1 Ответ

0 голосов
/ 14 июля 2020

Для туннелирования HTTP / 2 через прокси-сервер, который его не понимает, вам нужно использовать HTTP / 1.1 для начального соединения, а затем использовать HTTP / 2 только в туннеле. В вашем коде с самого начала используется HTTP / 2, что не сработает.

Чтобы фактически создать этот туннель, вы сначала отправляете запрос HTTP CONNECT для целевого хоста, получаете ответ 200 и тогда все остальное в соединении в будущем будет перенаправляться между вами и целевым хостом.

После того, как этот туннель заработает, вы можете отправить HTTP / 2 (или что-нибудь еще, что понимает целевой сервер), и он 'll 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 в узле, подобном этому, так что на них определенно стоит взглянуть. Конечно, это все еще в стадии разработки, поэтому дайте мне знать, если у вас возникнут какие-либо вопросы или обнаружите какие-либо ошибки.

...