nodejs / undici

An HTTP/1.1 client, written from scratch for Node.js
https://nodejs.github.io/undici
MIT License
6.23k stars 541 forks source link

Does not set TLS `servername` to the value of the `Host` header #3401

Closed vegerot closed 4 months ago

vegerot commented 4 months ago

Bug Description

I have seen it mentioned in a few issues (#332, #764), that we set the TLS servername to the value of the Host HTTP header. However, for me it's setting servername to the origin of the URL.

Reproducible By

let url = "https://www-teflon.walmart.com.akadns.net"
let desiredTLSServername = "www-teflon.walmart.com"
await fetch(url, {
  headers: {
    Host: desiredTLSServername
  }
})

Expected Behavior

Should receive HTTP 444

Actual Behavior

Uncaught TypeError: fetch failed
    at node:internal/deps/undici/undici:13178:13
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async REPL7:1:33 {
  [cause]: [Error: 004C4DF401000000:error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure:ssl/record/rec_layer_s3.c:907:SSL alert number 40
  ] {
    library: 'SSL routines',
    reason: 'ssl/tls alert handshake failure',
    code: 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE'
  }

Logs & Screenshots

image

Environment

Additional context

To avoid any XY problems, let me explain what I'm trying to do.

I am trying to make a fetch call to https://www-teflon.walmart.com.akadns.net. However, it is configured to only accept connections with the SNI of www-teflon.walmart.com. Reproduction to demonstrate:

$ openssl s_client -connect www-teflon.walmart.com.akadns.net:443 # ❌
Connecting to 20.85.116.226
CONNECTED(00000005)
004C4DF401000000:error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure:ssl/record/rec_layer_s3.c:907:SSL alert number 40

$ openssl s_client -connect www-teflon.walmart.com.akadns.net:443 -servername www-teflon.walmart.com # ✅
Connecting to 20.85.116.226
CONNECTED(00000005)
depth=2 OU=GlobalSign Root CA - R3, O=GlobalSign, CN=GlobalSign
verify return:1
depth=1 C=BE, O=GlobalSign nv-sa, CN=GlobalSign RSA OV SSL CA 2018
verify return:1
depth=0 C=US, ST=Arkansas, L=Bentonville, O=Walmart Inc., CN=origin-prod-ncf.walmart.com
verify return:1

I want to do this using fetch in Node.js.

vegerot commented 4 months ago

note: Axios (using the default node:http adapter) does the right thing here

let axios = (await import("axios")).default
let url = "https://www-teflon.walmart.com.akadns.net"
let desiredTLSServername = "www-teflon.walmart.com"
await axios.get(url, {
  headers: {
    Host: desiredTLSServername
  }
})
Uncaught AxiosError: Request failed with status code 444
    at settle (file:///Users/m0c0j7y/gecgithub01.walmart.com/r0r039p/page-bank/node_modules/axios/lib/core/settle.js:19:12)
    at IncomingMessage.handleStreamEnd (file:///Users/m0c0j7y/gecgithub01.walmart.com/r0r039p/page-bank/node_modules/axios/lib/adapters/http.js:589:11)
    at IncomingMessage.emit (node:events:532:35)
    at IncomingMessage.emit (node:domain:551:15)
    at endReadableNT (node:internal/streams/readable:1696:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
    at Axios.request (file:///Users/m0c0j7y/gecgithub01.walmart.com/r0r039p/page-bank/node_modules/axios/lib/core/Axios.js:45:41)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async REPL8:1:33
    at async node:repl:645:29 {
  code: 'ERR_BAD_REQUEST',

(again, we want a 444 here)

image

Deno behaves the same as Node, but Bun does what I want:

image


It looks like this behavior has changed recently

image

vegerot commented 4 months ago

Questions for maintainer:

KhafraDev commented 4 months ago
const Agent = globalThis[Symbol.for('undici.globalDispatcher.1')].constructor

globalThis[Symbol.for('undici.globalDispatcher.1')] = new Agent(...)
vegerot commented 4 months ago

Thank you. So this behavior was changed sometime between Node 20.5.1 and 20.12.1? And the two comments (#332, #764) I cited in the OP are no longer correct?

vegerot commented 4 months ago

Also, specifically for the Host header, this server I'm talking about requires me to set both the TLS servername and the Host header to a specific value. How would you recommend I do this?

ronag commented 4 months ago

Also, specifically for the Host header, this server I'm talking about requires me to set both the TLS servername and the Host header to a specific value. How would you recommend I do this?

Don't use fetch. Use undici.request.

vegerot commented 3 months ago

thanks! When I use undici.request, it doesn't decompress the response body. What would you recommend to do this? Is there a way to set the Host header without switching off of fetch?

metcoder95 commented 3 months ago

You'll need to manually decompress the body based on the headers. We have #3255 & #3274 in progress aiming for that.

vegerot commented 3 months ago

Thank you @metcoder95 ! I guess it's a little disappointing because fetch does almost what I want, but because I can't set the Host header I need to switch to a less convenient API.

I understand you want to strictly conform to the fetch spec, but I wish there was a special way to override the Host behavior. I get that it doesn't make sense, though.

I'll look into those issues, and may help with the implementation 🙂