[英]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.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
}
我用詳細的 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.