From 4f5f4ed22b1d0e5983586127fe92ba2ea0705f62 Mon Sep 17 00:00:00 2001 From: Christian Rose Date: Sun, 10 Mar 2024 15:04:27 +0100 Subject: [PATCH 01/11] fix: send correct SNI for proxy connections (#2939) --- docs/docs/api/Errors.md | 1 + lib/core/errors.js | 13 ++- lib/core/request.js | 5 +- lib/dispatcher/proxy-agent.js | 14 ++- package.json | 1 + test/fixtures/client-ca-crt.pem | 17 --- test/fixtures/client-crt-2048.pem | 22 ---- test/fixtures/client-crt.pem | 17 --- test/fixtures/client-key-2048.pem | 27 ----- test/fixtures/client-key.pem | 27 ----- test/proxy-agent.js | 172 +++++++++++++++++++++++++----- 11 files changed, 174 insertions(+), 142 deletions(-) delete mode 100644 test/fixtures/client-ca-crt.pem delete mode 100644 test/fixtures/client-crt-2048.pem delete mode 100644 test/fixtures/client-crt.pem delete mode 100644 test/fixtures/client-key-2048.pem delete mode 100644 test/fixtures/client-key.pem diff --git a/docs/docs/api/Errors.md b/docs/docs/api/Errors.md index 917e45df9fc..c32868912a6 100644 --- a/docs/docs/api/Errors.md +++ b/docs/docs/api/Errors.md @@ -26,6 +26,7 @@ import { errors } from 'undici' | `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header | | `InformationalError` | `UND_ERR_INFO` | expected error with reason | | `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed | +| `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed | ### `SocketError` diff --git a/lib/core/errors.js b/lib/core/errors.js index 0d0b7f60bc2..3d69fdbecba 100644 --- a/lib/core/errors.js +++ b/lib/core/errors.js @@ -195,6 +195,16 @@ class RequestRetryError extends UndiciError { } } +class SecureProxyConnectionError extends UndiciError { + constructor (cause, message, options) { + super(message, { cause, ...(options ?? {}) }) + this.name = 'SecureProxyConnectionError' + this.message = message || 'Secure Proxy Connection failed' + this.code = 'UND_ERR_PRX_TLS' + this.cause = cause + } +} + module.exports = { AbortError, HTTPParserError, @@ -216,5 +226,6 @@ module.exports = { ResponseContentLengthMismatchError, BalancedPoolMissingUpstreamError, ResponseExceededMaxSizeError, - RequestRetryError + RequestRetryError, + SecureProxyConnectionError } diff --git a/lib/core/request.js b/lib/core/request.js index 45f349c85c6..37839d3c949 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -40,7 +40,8 @@ class Request { bodyTimeout, reset, throwOnError, - expectContinue + expectContinue, + servername }, handler) { if (typeof path !== 'string') { throw new InvalidArgumentError('path must be a string') @@ -181,7 +182,7 @@ class Request { validateHandler(handler, method, upgrade) - this.servername = getServerName(this.host) + this.servername = servername || getServerName(this.host) this[kHandler] = handler diff --git a/lib/dispatcher/proxy-agent.js b/lib/dispatcher/proxy-agent.js index 9df39edb1aa..e06ce59523b 100644 --- a/lib/dispatcher/proxy-agent.js +++ b/lib/dispatcher/proxy-agent.js @@ -5,7 +5,7 @@ const { URL } = require('node:url') const Agent = require('./agent') const Pool = require('./pool') const DispatcherBase = require('./dispatcher-base') -const { InvalidArgumentError, RequestAbortedError } = require('../core/errors') +const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors') const buildConnector = require('../core/connect') const kAgent = Symbol('proxy agent') @@ -37,7 +37,7 @@ class ProxyAgent extends DispatcherBase { } const url = this.#getUrl(opts) - const { href, origin, port, protocol, username, password } = url + const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url this[kProxy] = { uri: href, protocol } this[kAgent] = new Agent(opts) @@ -78,7 +78,8 @@ class ProxyAgent extends DispatcherBase { headers: { ...this[kProxyHeaders], host: requestedHost - } + }, + servername: this[kProxyTls]?.servername || proxyHostname }) if (statusCode !== 200) { socket.on('error', () => {}).destroy() @@ -96,7 +97,12 @@ class ProxyAgent extends DispatcherBase { } this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) } catch (err) { - callback(err) + if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + // Throw a custom error to avoid loop in client.js#connect + callback(new SecureProxyConnectionError(err)) + } else { + callback(err) + } } } }) diff --git a/package.json b/package.json index 8fd41ef1358..087c6aa7bfe 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "jest": "^29.0.2", "jsdom": "^24.0.0", "jsfuzz": "^1.0.15", + "node-forge": "^1.3.1", "pre-commit": "^1.2.2", "proxy": "^2.1.1", "snazzy": "^9.0.0", diff --git a/test/fixtures/client-ca-crt.pem b/test/fixtures/client-ca-crt.pem deleted file mode 100644 index 3abfd04c0ac..00000000000 --- a/test/fixtures/client-ca-crt.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICqDCCAZACCQC0Hman8CosTDANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDApu -b2RlanMub3JnMCAXDTIyMDcxOTE2MzQwMloYDzIxMjIwNzIwMTYzNDAyWjAVMRMw -EQYDVQQDDApub2RlanMub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAyrmvIOhsVJAinUZ0Np4o5cPz09arWAZnnDsMnU0d+NtI0lWOFCnpzJbER9eB -gJpRkOdkcsQFr0OcalExG4lQrj+yGdtLGSXVcE0aNsVSBNbNgaLbOFWfpA4c7pTF -SBLJdJ7pZ2LDrM2mXaQA30di3INsZOvuTnDSAEE8bwxnM7jDnTCOGD4asgzgknHa -NqYWJqrfEPoMcEtThX9XjBLlRq5X3YFAR8SRbMQDt2xbDLWO8mGo/y4Ezp+ol9dP -OdkX3f728EIgfk8fM7rpvHzJb8E6NPdKK/kqCjQxRJ4RMsRqKwiTgPcEqut0L6Kg -jGoDvOnc3dZ2QBrxGTYPrgZF2QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA2DC4n -GNqQIABC82e3CovVH/LYB8M/PaqMwmXDI8kAKwk3j3lTHYD0WIyaFtCL4z/2GyDs -sgRmMlx5xVgXNv+8e793TMOqJ/0zixijguatR8r9GWdPAPhqCyCNrmUA26eyHEUV -Hx9mU7RNjv+qVe7fNXBkDorsyecclnDcxUd9k2C+RbjitnSKvhP64XqxAGk49HUH -3gw5uZw9uVlmD/dPSeKeSO4TX1HECH+WmPBKrBrcFGXNwGNzst8pFe3YVLLuseIq -4d5ngaOThGzVDJdsGIxhDfDBfH5FzDTMgEJxQQ3yXYwPR3zF4Ntn13oDkIu/vgbH -4n1eYIau6/1Y9OLX ------END CERTIFICATE----- diff --git a/test/fixtures/client-crt-2048.pem b/test/fixtures/client-crt-2048.pem deleted file mode 100644 index 6d07ec131fc..00000000000 --- a/test/fixtures/client-crt-2048.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDkzCCAnugAwIBAgIUF2CLbUCxPnxARRlO7pANiXtZoLIwDQYJKoZIhvcNAQEL -BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM -GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X -DTIyMDYwOTE0Mzc0N1oXDTI1MDMwNDE0Mzc0N1owWTELMAkGA1UEBhMCQVUxEzAR -BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 -IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEA4PbcFnMY0FC1wzsyMf04GhOx/KNcOalHu4Wy76Wys+WoJ6hO5z87 -ZIcmsg0hbys1l6DGxloTXeZwcBDoOndUg3FBZvAXRKimhXA7Qf31a9efq9GXic2W -7Kyn1jPa724Vkr/zzlWb5I/Qkk6xcQmEFCDhilbMtpnPz/BwOwn/2vbcbiHNirUk -Dn+s0pUcQlin1f2AR4Jq7/K1xsqjjB6cU0chuzrwzwrglQS7jpXQxCsRaAAIZQJB -DTVQBEo/skqWwv8xABlVQgolxABIX3Wc3RUk7xRItdWCMe92/BJCGhWVXb2hUCBu -y/yz5hX9p353JlxmXEKQlhfPzhcdDv2sdwIDAQABo1MwUTAdBgNVHQ4EFgQUQ0di -dFnBDLhSDgHpM+/KBn+WmI4wHwYDVR0jBBgwFoAUQ0didFnBDLhSDgHpM+/KBn+W -mI4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAoCQJci8G+cUF -n030frY/OgnLJXUGC2ed9Tu+dYhEE7XQkX9WO3IK8As+jGY0kKzX7ZsvWAHHbSa3 -8qmHh1vWflU9HEc0MO0Toy6Ale2/BCjs0Oy3q2vd6t9pl3Pq2JTHyJNYu44h45we -ufQ+ttylHGZSmAqeHz4yGp1xVvjbfriDYuc0kW9UTwMpdpzR9RmqQEVD4ySxpuYV -FTj/ZiY89GdIJvsz1pmAhTUcUfuMgSlWS1nt0YR4yMkFS8KqQ1iKEApjrdDCU48W -eABaPeTCUlBCFEDuKxFVPduYVVvOHtkX/8LPH3CO7EDMoSZ1iCDZ7b2+AZbwh9j+ -dXqw+WFi7w== ------END CERTIFICATE----- diff --git a/test/fixtures/client-crt.pem b/test/fixtures/client-crt.pem deleted file mode 100644 index 2bd94dfde7d..00000000000 --- a/test/fixtures/client-crt.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICpDCCAYwCCQCWvC2NnLEpZjANBgkqhkiG9w0BAQUFADAVMRMwEQYDVQQDDApu -b2RlanMub3JnMCAXDTIyMDcxOTE2NDE1OFoYDzIxMjIwNzIwMTY0MTU4WjARMQ8w -DQYDVQQLDAZVbmRpY2kwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDR -SJvCSXTHrmnGz/CN94nxgmnUD17jYzfJH+lbcJkw4RDHpb6KZ85LEijeKoYoGw+c -Z7a4LfmpIR4rcN3sJWGvafJyFx4DtLYPZiNrCaMsdMWiHbbMwrpvSsf5Fq3vVeUz -Py7wxzSRiM4VOwZ7fhCJdj2YIeQJgeIZh+NN/4mpyWehS4hQSHG+cbS4c44vkET0 -Hv48G7m+4ULFCZzmG2AIW8Drh73Wymmm3kymD3kDCAY4SDSJDArxNt6lJ3sGJGO6 -jobefLFyqvLj5544Lvk4C8hD3O+e9M3OHcdyqRXf55dZ8SIWgpoGVGXb5V5g3WL/ -ncXF87jm05pMZXqOz0wdAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAK2YxxGEDgqG -tp8uX/n0nFAj1p8sfkuD+FqYg7+PN/HYqCq6Ibrz/vVABL5Khb4qQzZN/ckJhY3k -bfwEjRTOoXMhPv+IkShMDdbTunwSQUXqeLe+qmPbLt5ZccxcYVIzEhJMlnjeJ4nk -NHg3BXt8y6mIIfY0Sv4znTkV995GHLK3Ax/Fd/2aio6aRCzkBCdaXY8j0SOzFHVy -+AvgRj04K2yBEEHd4bQTdLCJQR/gFQnGj37gXQp9I4qq+/1qj4sTs8BufnGKTDVT -/jYeycIY3l4A8/72NmDSIohaJTPwFUoXNBYywOnW71+Y05PXT45lJuaOJUf2s9iH -p/eTiEsfHsk= ------END CERTIFICATE----- diff --git a/test/fixtures/client-key-2048.pem b/test/fixtures/client-key-2048.pem deleted file mode 100644 index b7dffa66fe0..00000000000 --- a/test/fixtures/client-key-2048.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA4PbcFnMY0FC1wzsyMf04GhOx/KNcOalHu4Wy76Wys+WoJ6hO -5z87ZIcmsg0hbys1l6DGxloTXeZwcBDoOndUg3FBZvAXRKimhXA7Qf31a9efq9GX -ic2W7Kyn1jPa724Vkr/zzlWb5I/Qkk6xcQmEFCDhilbMtpnPz/BwOwn/2vbcbiHN -irUkDn+s0pUcQlin1f2AR4Jq7/K1xsqjjB6cU0chuzrwzwrglQS7jpXQxCsRaAAI -ZQJBDTVQBEo/skqWwv8xABlVQgolxABIX3Wc3RUk7xRItdWCMe92/BJCGhWVXb2h -UCBuy/yz5hX9p353JlxmXEKQlhfPzhcdDv2sdwIDAQABAoIBAFVfeaCPZ2BO8Nu5 -UFBGP48t4EL3H93GDzHsCD8IC+xXgFwkdGUvyvNYkufJMeIFbN4xJp5JusXM2Oi+ -kdL2TD1hsqdFAB+PPTqwn9xoa0XU24SSEsc6HUeOMleI8FIi3c8GR5kLRhEUPtv3 -P0GdkeEtpUohrKizcHkCTyUoo09N35MFoH3Nb1iyMd10uq0iQlusljkTuukcHstK -MZQAYYcslqzyz9468O/cvsk23Ynd5FfjLgYKmdJ09qaxm4ptnF9NNJ2cLqwElbUF -xI3H5L/t1zxdwI0xZFFgDA4Ccpeq9QsRhRJGAOV94tN+4PxWXEPeQk4PM1EFDrNU -yysi/XkCgYEA+ElKG6cWQZydsb5Tk1vdJ/k18gZa5sv+WUGXkfm9EVecftGjtKQO -c7GwHO1IsLoZkhKfPpa/oifBR97DZRzw1ManEQPS980TZYei3Y9/8uPEpvgvRmm9 -MCHA5wp6YMlkZ5VN0SBRWnPhLtZ8L2/cqHOUCQf6YsIJU9/fewufrbUCgYEA5/QU -/tDBDl/f4A2R1HlIkGd1jS//CJLCc3riy0SQxcWIq6/cqflyfvRWiax5DwcO7qfh -3WbJldu9H0IWZjBCqX0v/jHvWBzaKNQCKbFFcL76Lr8bJCwlUMTH9MOhHf3uCOHD -J7YSTVJdvgzLN8K6yFhc0gI4VYQtnQTWJENObPsCgYEAlawAq6jO5uCVw3dbhGKF -cDpwBaVFGQpyGrZKu6nUCudIpL6VtCiNubqs0tNL1ZVqIr9tFdrkTMkwX7XvDj4j -A/F49u3aOJ18iuD4Eh4WYIJjos/MF+NYM/K1CdIsMbpV94dusJmN0Tw3y/dqR2Jk -n3uFCuivTOdxngk//DnmmV0CgYEA1CXNUiZSfLg5xe4DVEc9lD3cKS8d3pSEXySk -6+8hTpHV59moRJpPG0iVIcRq0NDO2n8YOOy7MWJSPpWucPZw8h362E6Jr5hr/G20 -MLffYDh8EGdgBpyN4Kqqi/allQ3cOalrWhXP9YKBFMMU10I2nekbtESti6GiKnvy -9CXPRCMCgYBZ2w+VVdhUUBA/elbuEdfbPwIYVDAk31PYg0c9jvQVusmfD1CuY/51 -JVsF5oJSosiN7WdDIETkklth0q3lAsQBKoYYMUw54RBf6FawoumB6MVdc3u4y9Ko -l9JC9czdEqb/e0LBqFiWsrtPk9WQf2gyN1mIXQPbyTT1O1J+DvUIbQ== ------END RSA PRIVATE KEY----- diff --git a/test/fixtures/client-key.pem b/test/fixtures/client-key.pem deleted file mode 100644 index 6b475243a4f..00000000000 --- a/test/fixtures/client-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA0Uibwkl0x65pxs/wjfeJ8YJp1A9e42M3yR/pW3CZMOEQx6W+ -imfOSxIo3iqGKBsPnGe2uC35qSEeK3Dd7CVhr2nychceA7S2D2YjawmjLHTFoh22 -zMK6b0rH+Rat71XlMz8u8Mc0kYjOFTsGe34QiXY9mCHkCYHiGYfjTf+JqclnoUuI -UEhxvnG0uHOOL5BE9B7+PBu5vuFCxQmc5htgCFvA64e91spppt5Mpg95AwgGOEg0 -iQwK8TbepSd7BiRjuo6G3nyxcqry4+eeOC75OAvIQ9zvnvTNzh3HcqkV3+eXWfEi -FoKaBlRl2+VeYN1i/53FxfO45tOaTGV6js9MHQIDAQABAoIBACOp2+Ef42ajsiLP -DI8kv70IHECm3eSh47/CUGHkrjZGJDXhaLbtOZpRXeV+GZ57/g0JH3oDW6gWnK2K -bkbvl9XsmAQZLGQ1R1EYdrCm08efno4hwiTiiiKs+6bW1o0Sdhxlh/o/+BVU2smD -ZXdl5CuImrZyEAoOuBjhrzp7cVodSOYYK2RIAL35oAtKLR6NE40XGcxQSCdm+1eU -PzRo8TimQxujyIHrd1QV2FirmLfDFGg3LN8DS72n26bhvDg3PF6PVMF20BKTDqiu -xAyKg3weBsee2QoyegDRdgTD1PvjwWqqnsntPbvY5V8PR1DDmssfotYToNPVuJd2 -6usmBAECgYEA/21NZPZJdxRKwCiWXoqBUIY0VFajxihVxZ9pIZPXOFhpGmyj/jf6 -jBiHAqtucRdABtNxqsztGbEzJsMyNv7MqEVTAWUPH804OwW/C6Z2011GZ1AUN05n -zTxPR4eCYlxvSM+wwC8q+4mSo7hAZj5HltUI0kfEahZnGXqG4FRC1TUCgYEA0cDO -DuTrytk6EoYYCsS7ps87MYUlU97RHFrRGwf+V1Rz2RCz+XAkYCI1/tOpb0VeF1de -fX1mlM3edkLX2ooylYxv5HKPpICzPXeGK/u/HaJBRyZEq6Ms0HK8XyJOdG/UyuiZ -p9nc8eaZYvco24bT4dWe5oZ43mnydAwyK2tOgEkCgYEA/blJg9zSJSNXDYJDvC3B -PofRO2XE0XYHnYM4H06IH0RTQxhf3oskqj1C/3fjARujUiR/aLafX0ISGZMUMmTw -TsZuKZiFaYWlMZwHpj75EgQ5hy6YpkeP/OLHrboB3ksLkDweywkPnUWPEGpaLjX3 -TvDXDmqTxP3z8+8uQ2/v43ECgYB5/3BaTV+vviT+vSuip8aVQRcmuFB7ta9elJvm -4wFV/fLbn9FuFYGywHMzYhy8cVZGsTRuPM+7YPoxQrOVkqfVP7ec4d0WSxz1dV1+ -m5APRl49ac6rHd9k5jcWBjgnlRvpYNxuOlM+B2fTnfoPpR37zmn7nt8STgEM6kML -6f/gsQKBgFJH95hEgqhfEHmP23+ZWH0Dl7zD5sJJe4CYTgYriNeKKzpz2G6OVv+U -xNc8eGbnr4raPTxCCLKz6XJhuQuPQDpkoHvkhjOqZ5Tbb4fCaLdcVE0vwqBE1gGk -ryKSvahgHIykq3+RYpL4u2xypx81IBOMk7EM++Z6gdYMq0ZTN/fL ------END RSA PRIVATE KEY----- diff --git a/test/proxy-agent.js b/test/proxy-agent.js index 200f3066c6a..bc7be726700 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -3,15 +3,98 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { request, fetch, setGlobalDispatcher, getGlobalDispatcher } = require('..') -const { InvalidArgumentError } = require('../lib/core/errors') -const { readFileSync } = require('node:fs') -const { join } = require('node:path') +const { InvalidArgumentError, SecureProxyConnectionError } = require('../lib/core/errors') const ProxyAgent = require('../lib/dispatcher/proxy-agent') const Pool = require('../lib/dispatcher/pool') const { createServer } = require('node:http') const https = require('node:https') const { createProxy } = require('proxy') +const certs = (() => { + const forge = require('node-forge') + const createCert = (cn, issuer, keyLength = 2048) => { + const keys = forge.pki.rsa.generateKeyPair(keyLength) + const cert = forge.pki.createCertificate() + cert.publicKey = keys.publicKey + cert.serialNumber = '' + Date.now() + cert.validity.notBefore = new Date() + cert.validity.notAfter = new Date() + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10) + + const attrs = [{ + name: 'commonName', + value: cn + }] + cert.setSubject(attrs) + const isCa = issuer === undefined + cert.setExtensions([{ + name: 'basicConstraints', + cA: isCa + }, { + name: 'keyUsage', + keyCertSign: true, + digitalSignature: true, + nonRepudiation: true, + keyEncipherment: true, + dataEncipherment: true + }, { + name: 'extKeyUsage', + serverAuth: true, + clientAuth: true, + codeSigning: true, + emailProtection: true, + timeStamping: true + }, { + name: 'nsCertType', + client: true, + server: true, + email: true, + objsign: true, + sslCA: isCa, + emailCA: isCa, + objCA: isCa + }]) + + const alg = forge.md.sha256.create() + if (issuer !== undefined) { + cert.setIssuer(issuer.certificate.subject.attributes) + cert.sign(issuer.privateKey, alg) + } else { + cert.setIssuer(attrs) + cert.sign(keys.privateKey, alg) + } + return { + privateKey: keys.privateKey, + publicKey: keys.publicKey, + certificate: cert + } + } + + const root = createCert('CA') + const server = createCert('agent1', root) + const client = createCert('client', root) + const proxy = createCert('proxy', root) + + return { + root: { + key: forge.pki.privateKeyToPem(root.privateKey), + crt: forge.pki.certificateToPem(root.certificate) + }, + server: { + key: forge.pki.privateKeyToPem(server.privateKey), + crt: forge.pki.certificateToPem(server.certificate) + }, + client: { + key: forge.pki.privateKeyToPem(client.privateKey), + crt: forge.pki.certificateToPem(client.certificate) + }, + proxy: { + key: forge.pki.privateKeyToPem(proxy.privateKey), + crt: forge.pki.certificateToPem(proxy.certificate) + } + } +})() + test('should throw error when no uri is provided', (t) => { t = tspl(t, { plan: 2 }) t.throws(() => new ProxyAgent(), InvalidArgumentError) @@ -527,10 +610,8 @@ test('Proxy via HTTP to HTTPS endpoint', async (t) => { uri: proxyUrl, requestTls: { ca: [ - readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8') + certs.root.crt ], - key: readFileSync(join(__dirname, 'fixtures', 'client-key-2048.pem'), 'utf8'), - cert: readFileSync(join(__dirname, 'fixtures', 'client-crt-2048.pem'), 'utf8'), servername: 'agent1' } }) @@ -579,19 +660,14 @@ test('Proxy via HTTPS to HTTPS endpoint', async (t) => { uri: proxyUrl, proxyTls: { ca: [ - readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8') + certs.root.crt ], - key: readFileSync(join(__dirname, 'fixtures', 'client-key-2048.pem'), 'utf8'), - cert: readFileSync(join(__dirname, 'fixtures', 'client-crt-2048.pem'), 'utf8'), - servername: 'agent1', - rejectUnauthorized: false + servername: 'proxy' }, requestTls: { ca: [ - readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8') + certs.root.crt ], - key: readFileSync(join(__dirname, 'fixtures', 'client-key-2048.pem'), 'utf8'), - cert: readFileSync(join(__dirname, 'fixtures', 'client-crt-2048.pem'), 'utf8'), servername: 'agent1' } }) @@ -640,12 +716,9 @@ test('Proxy via HTTPS to HTTP endpoint', async (t) => { uri: proxyUrl, proxyTls: { ca: [ - readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8') + certs.root.crt ], - key: readFileSync(join(__dirname, 'fixtures', 'client-key-2048.pem'), 'utf8'), - cert: readFileSync(join(__dirname, 'fixtures', 'client-crt-2048.pem'), 'utf8'), - servername: 'agent1', - rejectUnauthorized: false + servername: 'proxy' } }) @@ -720,6 +793,55 @@ test('Proxy via HTTP to HTTP endpoint', async (t) => { proxyAgent.close() }) +test('Proxy via HTTPS to HTTP fails on wrong SNI', async (t) => { + t = tspl(t, { plan: 2 }) + const server = await buildServer() + const proxy = await buildSSLProxy() + + const serverUrl = `http://localhost:${server.address().port}` + const proxyUrl = `https://localhost:${proxy.address().port}` + const proxyAgent = new ProxyAgent({ + uri: proxyUrl, + proxyTls: { + ca: [ + certs.root.crt + ] + } + }) + + server.on('request', function (req, res) { + t.ok(!req.connection.encrypted) + res.end(JSON.stringify(req.headers)) + }) + + server.on('secureConnection', () => { + t.fail('server is http') + }) + + proxy.on('secureConnection', () => { + t.fail('proxy is http') + }) + + proxy.on('connect', () => { + t.ok(true, 'connect to proxy') + }) + + proxy.on('request', function () { + t.fail('proxy should never receive requests') + }) + + try { + await request(serverUrl, { dispatcher: proxyAgent }) + } catch (e) { + t.ok(e instanceof SecureProxyConnectionError) + t.ok(e.cause.code === 'ERR_TLS_CERT_ALTNAME_INVALID') + } + + server.close() + proxy.close() + proxyAgent.close() +}) + function buildServer () { return new Promise((resolve) => { const server = createServer() @@ -730,10 +852,10 @@ function buildServer () { function buildSSLServer () { const serverOptions = { ca: [ - readFileSync(join(__dirname, 'fixtures', 'client-ca-crt.pem'), 'utf8') + certs.root.crt ], - key: readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8'), - cert: readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8') + key: certs.server.key, + cert: certs.server.crt } return new Promise((resolve) => { const server = https.createServer(serverOptions) @@ -753,10 +875,10 @@ function buildProxy (listener) { function buildSSLProxy () { const serverOptions = { ca: [ - readFileSync(join(__dirname, 'fixtures', 'client-ca-crt.pem'), 'utf8') + certs.root.crt ], - key: readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8'), - cert: readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8') + key: certs.proxy.key, + cert: certs.proxy.crt } return new Promise((resolve) => { From 3d4cf43f45b1f87a8d4f68381f5172874ae25d60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 10 Mar 2024 15:12:25 +0100 Subject: [PATCH 02/11] build(deps): bump node from `8bf9240` to `7bfef1d` in /build (#2937) Bumps node from `8bf9240` to `7bfef1d`. --- updated-dependencies: - dependency-name: node dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Dockerfile b/build/Dockerfile index 4faceb043a4..6607187620e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -FROM node:21-alpine@sha256:8bf9240217fe9cf26456fa685bca1e1b92f8fe6995c8cf73c9876b9d6960bd35 +FROM node:21-alpine@sha256:7bfef1d72befbb72b0894a3e4503edbdc0441058b4d091325143338cbf54cff8 ARG UID=1000 ARG GID=1000 From 70f2871ff863fb723d22a68be2c16859a1f1cad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Can=20Alt=C4=B1n?= Date: Sun, 10 Mar 2024 18:07:17 +0300 Subject: [PATCH 03/11] fetch: improve util.inspect output for web specifications (#2938) --- lib/web/fetch/headers.js | 11 +++++++++++ test/fetch/headers-inspect-custom.js | 17 +++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 test/fetch/headers-inspect-custom.js diff --git a/lib/web/fetch/headers.js b/lib/web/fetch/headers.js index b3ec5a70711..b2580e90e04 100644 --- a/lib/web/fetch/headers.js +++ b/lib/web/fetch/headers.js @@ -12,6 +12,7 @@ const { } = require('./util') const { webidl } = require('./webidl') const assert = require('node:assert') +const util = require('util') const kHeadersMap = Symbol('headers map') const kHeadersSortedMap = Symbol('headers map sorted') @@ -576,8 +577,18 @@ class Headers { return this[kHeadersList] } + + [util.inspect.custom] (depth, options) { + const inspected = util.inspect(this[kHeadersList].entries) + + return `Headers ${inspected}` + } } +Object.defineProperty(Headers.prototype, util.inspect.custom, { + enumerable: false +}) + iteratorMixin('Headers', Headers, kHeadersSortedMap, 0, 1) Object.defineProperties(Headers.prototype, { diff --git a/test/fetch/headers-inspect-custom.js b/test/fetch/headers-inspect-custom.js new file mode 100644 index 00000000000..1aa3326e98c --- /dev/null +++ b/test/fetch/headers-inspect-custom.js @@ -0,0 +1,17 @@ +'use strict' + +const { Headers } = require('../../lib/web/fetch/headers') +const { test } = require('node:test') +const assert = require('node:assert') +const util = require('util') + +test('Headers class custom inspection', () => { + const headers = new Headers() + headers.set('Content-Type', 'application/json') + headers.set('Authorization', 'Bearer token') + + const inspectedOutput = util.inspect(headers, { depth: 1 }) + + const expectedOutput = "Headers { 'Content-Type': 'application/json', Authorization: 'Bearer token' }" + assert.strictEqual(inspectedOutput, expectedOutput) +}) From ed6c734fa22eacde4e7a16cb5c7eecc154fc8ec5 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sun, 10 Mar 2024 17:47:03 +0100 Subject: [PATCH 04/11] ci: fix broken ci on windows and node v21 because of libuv bug (#2941) --- test/node-test/debug.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/node-test/debug.js b/test/node-test/debug.js index ade1843ec36..3e6ca0bc0ef 100644 --- a/test/node-test/debug.js +++ b/test/node-test/debug.js @@ -8,10 +8,7 @@ const { tspl } = require('@matteo.collina/tspl') // eslint-disable-next-line no-control-regex const removeEscapeColorsRE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g -// see https://github.com/nodejs/node/issues/51766 -const skip = process.version === 'v21.6.2' && process.platform === 'win32' - -test('debug#websocket', { skip }, async t => { +test('debug#websocket', async t => { const assert = tspl(t, { plan: 8 }) const child = spawn( process.execPath, @@ -48,10 +45,9 @@ test('debug#websocket', { skip }, async t => { }) await assert.completed - child.kill() }) -test('debug#fetch', { skip }, async t => { +test('debug#fetch', async t => { const assert = tspl(t, { plan: 7 }) const child = spawn( process.execPath, @@ -83,10 +79,9 @@ test('debug#fetch', { skip }, async t => { }) await assert.completed - child.kill() }) -test('debug#undici', { skip }, async t => { +test('debug#undici', async t => { // Due to Node.js webpage redirect const assert = tspl(t, { plan: 7 }) const child = spawn( @@ -121,7 +116,6 @@ test('debug#undici', { skip }, async t => { }) await assert.completed - child.kill() }) function extractLines (chunks) { From 7da8232df9fbf0cc2fa41e86a8f14cb3dd7f53c7 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sun, 10 Mar 2024 18:39:40 +0100 Subject: [PATCH 05/11] perf: improve getResolveErrorBodyCallback (#2940) --- benchmarks/api/util.mjs | 37 ++++++++++++++++++++++ lib/api/util.js | 68 ++++++++++++++++++++++++++++++++--------- 2 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 benchmarks/api/util.mjs diff --git a/benchmarks/api/util.mjs b/benchmarks/api/util.mjs new file mode 100644 index 00000000000..34c54015ed8 --- /dev/null +++ b/benchmarks/api/util.mjs @@ -0,0 +1,37 @@ +import { bench, group, run } from 'mitata' +import { isContentTypeText, isContentTypeApplicationJson } from '../../lib/api/util.js' + +const html = 'text/html' +const json = 'application/json; charset=UTF-8' + +group('isContentTypeText', () => { + bench(`isContentTypeText('${html}')`, () => { + return isContentTypeText(html) + }) + bench(`isContentTypeText('${json}')`, () => { + return isContentTypeText(json) + }) + bench('html.startsWith(\'text/\')', () => { + return html.startsWith('text/') + }) + bench('json.startsWith(\'text/\')', () => { + return json.startsWith('text/') + }) +}) + +group('isContentTypeApplicationJson', () => { + bench(`isContentTypeApplicationJson('${html}')`, () => { + return isContentTypeApplicationJson(html) + }) + bench(`isContentTypeApplicationJson('${json}')`, () => { + return isContentTypeApplicationJson(json) + }) + bench('html.startsWith(\'application/json\')', () => { + return html.startsWith('application/json') + }) + bench('json.startsWith(\'application/json\')', () => { + return json.startsWith('application/json') + }) +}) + +await run() diff --git a/lib/api/util.js b/lib/api/util.js index 24f69d12bd3..c5573bf3f8c 100644 --- a/lib/api/util.js +++ b/lib/api/util.js @@ -21,28 +21,66 @@ async function getResolveErrorBodyCallback ({ callback, body, contentType, statu } } + const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}` + if (statusCode === 204 || !contentType || !chunks) { - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) + queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers))) return } - try { - if (contentType.startsWith('application/json')) { - const payload = JSON.parse(chunksDecode(chunks, length)) - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) - return - } + const stackTraceLimit = Error.stackTraceLimit + Error.stackTraceLimit = 0 + let payload - if (contentType.startsWith('text/')) { - const payload = chunksDecode(chunks, length) - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) - return + try { + if (isContentTypeApplicationJson(contentType)) { + payload = JSON.parse(chunksDecode(chunks, length)) + } else if (isContentTypeText(contentType)) { + payload = chunksDecode(chunks, length) } - } catch (err) { - // Process in a fallback if error + } catch { + // process in a callback to avoid throwing in the microtask queue + } finally { + Error.stackTraceLimit = stackTraceLimit } + queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers, payload))) +} + +const isContentTypeApplicationJson = (contentType) => { + return ( + contentType.length > 15 && + contentType[11] === '/' && + contentType[0] === 'a' && + contentType[1] === 'p' && + contentType[2] === 'p' && + contentType[3] === 'l' && + contentType[4] === 'i' && + contentType[5] === 'c' && + contentType[6] === 'a' && + contentType[7] === 't' && + contentType[8] === 'i' && + contentType[9] === 'o' && + contentType[10] === 'n' && + contentType[12] === 'j' && + contentType[13] === 's' && + contentType[14] === 'o' && + contentType[15] === 'n' + ) +} - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) +const isContentTypeText = (contentType) => { + return ( + contentType.length > 4 && + contentType[4] === '/' && + contentType[0] === 't' && + contentType[1] === 'e' && + contentType[2] === 'x' && + contentType[3] === 't' + ) } -module.exports = { getResolveErrorBodyCallback } +module.exports = { + getResolveErrorBodyCallback, + isContentTypeApplicationJson, + isContentTypeText +} From e81dc8da1755418f801431546026e3c9bcf91a43 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sun, 10 Mar 2024 20:47:38 +0100 Subject: [PATCH 06/11] fix: don't assign kAgent twice (#2942) --- lib/dispatcher/proxy-agent.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/dispatcher/proxy-agent.js b/lib/dispatcher/proxy-agent.js index e06ce59523b..fda87db96a4 100644 --- a/lib/dispatcher/proxy-agent.js +++ b/lib/dispatcher/proxy-agent.js @@ -40,7 +40,6 @@ class ProxyAgent extends DispatcherBase { const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url this[kProxy] = { uri: href, protocol } - this[kAgent] = new Agent(opts) this[kInterceptors] = opts.interceptors?.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent) ? opts.interceptors.ProxyAgent : [] From 825e6e11a134305d124eaf76444299a476eff057 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 11 Mar 2024 08:13:52 +0100 Subject: [PATCH 07/11] perf: dump immediatly if known size exceeds limit (#2882) * perf: dump immediatly if known size exceeds limit * fixup --- lib/api/api-request.js | 3 ++- lib/api/readable.js | 11 +++++++++-- test/client-request.js | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/lib/api/api-request.js b/lib/api/api-request.js index 2537b264ee3..92c8c84228e 100644 --- a/lib/api/api-request.js +++ b/lib/api/api-request.js @@ -91,7 +91,8 @@ class RequestHandler extends AsyncResource { const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers const contentType = parsedHeaders['content-type'] - const body = new Readable({ resume, abort, contentType, highWaterMark }) + const contentLength = parsedHeaders['content-length'] + const body = new Readable({ resume, abort, contentType, contentLength, highWaterMark }) this.callback = null this.res = body diff --git a/lib/api/readable.js b/lib/api/readable.js index f4d935c5bf6..796c237e889 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -11,8 +11,9 @@ const { ReadableStreamFrom } = require('../core/util') const kConsume = Symbol('kConsume') const kReading = Symbol('kReading') const kBody = Symbol('kBody') -const kAbort = Symbol('abort') +const kAbort = Symbol('kAbort') const kContentType = Symbol('kContentType') +const kContentLength = Symbol('kContentLength') const noop = () => {} @@ -21,6 +22,7 @@ class BodyReadable extends Readable { resume, abort, contentType = '', + contentLength, highWaterMark = 64 * 1024 // Same as nodejs fs streams. }) { super({ @@ -35,6 +37,7 @@ class BodyReadable extends Readable { this[kConsume] = null this[kBody] = null this[kContentType] = contentType + this[kContentLength] = contentLength // Is stream being consumed through Readable API? // This is an optimization so that we avoid checking @@ -146,7 +149,7 @@ class BodyReadable extends Readable { } async dump (opts) { - let limit = Number.isFinite(opts?.limit) ? opts.limit : 262144 + let limit = Number.isFinite(opts?.limit) ? opts.limit : 128 * 1024 const signal = opts?.signal if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) { @@ -160,6 +163,10 @@ class BodyReadable extends Readable { } return await new Promise((resolve, reject) => { + if (this[kContentLength] > limit) { + this.destroy(new AbortError()) + } + const onAbort = () => { this.destroy(signal.reason ?? new AbortError()) } diff --git a/test/client-request.js b/test/client-request.js index 2b73d3b01d0..a0f2d43ba9d 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -14,6 +14,41 @@ const { promisify } = require('node:util') const { NotSupportedError } = require('../lib/core/errors') const { parseFormDataString } = require('./utils/formdata') +test('request dump big', async (t) => { + t = tspl(t, { plan: 3 }) + + const server = createServer((req, res) => { + res.setHeader('content-length', 999999999) + while (res.write('asd')) { + // Do nothing... + } + res.on('drain', () => t.fail()) + }) + after(() => server.close()) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.destroy()) + + let dumped = false + client.on('disconnect', () => { + t.strictEqual(dumped, true) + }) + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.dump().then(() => { + dumped = true + t.ok(true, 'pass') + }) + }) + }) + + await t.completed +}) + test('request dump', async (t) => { t = tspl(t, { plan: 3 }) From 94598c267f8be84510d059d6ce06dcf317f220bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 23:46:07 +0100 Subject: [PATCH 08/11] build(deps): bump node from `7bfef1d` to `4999fa1` in /build (#2946) Bumps node from `7bfef1d` to `4999fa1`. --- updated-dependencies: - dependency-name: node dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Dockerfile b/build/Dockerfile index 6607187620e..d74cd6d44f1 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -FROM node:21-alpine@sha256:7bfef1d72befbb72b0894a3e4503edbdc0441058b4d091325143338cbf54cff8 +FROM node:21-alpine@sha256:4999fa1391e09259e71845d3d0e9ddfe5f51ab30253c8b490c633f710c7446a0 ARG UID=1000 ARG GID=1000 From 0b5a4515f3b7c0bbdc873ea172c47a56e515b167 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Tue, 12 Mar 2024 07:02:19 +0100 Subject: [PATCH 09/11] try to fix windows failure (#2950) Refs: https://github.com/nodejs/undici/issues/2949 --- test/client-request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/client-request.js b/test/client-request.js index a0f2d43ba9d..8e0111de09c 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -22,7 +22,6 @@ test('request dump big', async (t) => { while (res.write('asd')) { // Do nothing... } - res.on('drain', () => t.fail()) }) after(() => server.close()) @@ -39,6 +38,7 @@ test('request dump big', async (t) => { method: 'GET' }, (err, { body }) => { t.ifError(err) + body.on('data', () => t.fail()) body.dump().then(() => { dumped = true t.ok(true, 'pass') From 4c1714b0c07e7d5ee371121a942614f05c26cf69 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 13 Mar 2024 09:04:55 +0900 Subject: [PATCH 10/11] perf: improve parsing form-data (#2944) * perf: improve parsing form-data * apply suggestions from code review * apply suggestions from code review --- lib/core/util.js | 12 ++++++++- lib/web/fetch/formdata-parser.js | 42 ++++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index cbb6d7495e5..a62396e23e0 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -440,7 +440,8 @@ function addAbortListener (signal, listener) { return () => signal.removeListener('abort', listener) } -const hasToWellFormed = !!String.prototype.toWellFormed +const hasToWellFormed = typeof String.prototype.toWellFormed === 'function' +const hasIsWellFormed = typeof String.prototype.isWellFormed === 'function' /** * @param {string} val @@ -449,6 +450,14 @@ function toUSVString (val) { return hasToWellFormed ? `${val}`.toWellFormed() : nodeUtil.toUSVString(val) } +/** + * @param {string} val + */ +// TODO: move this to webidl +function isUSVString (val) { + return hasIsWellFormed ? `${val}`.isWellFormed() : toUSVString(val) === `${val}` +} + /** * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 * @param {number} c @@ -538,6 +547,7 @@ module.exports = { isErrored, isReadable, toUSVString, + isUSVString, isReadableAborted, isBlobLike, parseOrigin, diff --git a/lib/web/fetch/formdata-parser.js b/lib/web/fetch/formdata-parser.js index 8269dce42e9..b889fbf898d 100644 --- a/lib/web/fetch/formdata-parser.js +++ b/lib/web/fetch/formdata-parser.js @@ -1,12 +1,12 @@ 'use strict' -const { webidl } = require('./webidl') +const { toUSVString, isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util') const { utf8DecodeBytes } = require('./util') const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url') const { isFileLike, File: UndiciFile } = require('./file') const { makeEntry } = require('./formdata') const assert = require('node:assert') -const { isAscii, File: NodeFile } = require('node:buffer') +const { File: NodeFile } = require('node:buffer') const File = globalThis.File ?? NodeFile ?? UndiciFile @@ -15,6 +15,18 @@ const filenameBuffer = Buffer.from('; filename') const dd = Buffer.from('--') const ddcrlf = Buffer.from('--\r\n') +/** + * @param {string} chars + */ +function isAsciiString (chars) { + for (let i = 0; i < chars.length; ++i) { + if ((chars.charCodeAt(i) & ~0x7F) !== 0) { + return false + } + } + return true +} + /** * @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary * @param {string} boundary @@ -30,7 +42,7 @@ function validateBoundary (boundary) { // - it is composed by bytes in the ranges 0x30 to 0x39, 0x41 to 0x5A, or // 0x61 to 0x7A, inclusive (ASCII alphanumeric), or which are 0x27 ('), // 0x2D (-) or 0x5F (_). - for (let i = 0; i < boundary.length; i++) { + for (let i = 0; i < length; ++i) { const cp = boundary.charCodeAt(i) if (!( @@ -58,12 +70,12 @@ function escapeFormDataName (name, encoding = 'utf-8', isFilename = false) { // 1. If isFilename is true: if (isFilename) { // 1.1. Set name to the result of converting name into a scalar value string. - name = webidl.converters.USVString(name) + name = toUSVString(name) } else { // 2. Otherwise: // 2.1. Assert: name is a scalar value string. - assert(name === webidl.converters.USVString(name)) + assert(isUSVString(name)) // 2.2. Replace every occurrence of U+000D (CR) not followed by U+000A (LF), // and every occurrence of U+000A (LF) not preceded by U+000D (CR), in @@ -94,14 +106,16 @@ function multipartFormDataParser (input, mimeType) { // 1. Assert: mimeType’s essence is "multipart/form-data". assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data') + const boundaryString = mimeType.parameters.get('boundary') + // 2. If mimeType’s parameters["boundary"] does not exist, return failure. // Otherwise, let boundary be the result of UTF-8 decoding mimeType’s // parameters["boundary"]. - if (!mimeType.parameters.has('boundary')) { + if (boundaryString === undefined) { return 'failure' } - const boundary = Buffer.from(`--${mimeType.parameters.get('boundary')}`, 'utf8') + const boundary = Buffer.from(`--${boundaryString}`, 'utf8') // 3. Let entry list be an empty entry list. const entryList = [] @@ -200,7 +214,10 @@ function multipartFormDataParser (input, mimeType) { contentType ??= 'text/plain' // 5.10.2. If contentType is not an ASCII string, set contentType to the empty string. - if (!isAscii(Buffer.from(contentType))) { + + // Note: `buffer.isAscii` can be used at zero-cost, but converting a string to a buffer is a high overhead. + // Content-Type is a relatively small string, so it is faster to use `String#charCodeAt`. + if (!isAsciiString(contentType)) { contentType = '' } @@ -214,8 +231,8 @@ function multipartFormDataParser (input, mimeType) { } // 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object. - assert(name === webidl.converters.USVString(name)) - assert((typeof value === 'string' && value === webidl.converters.USVString(value)) || isFileLike(value)) + assert(isUSVString(name)) + assert((typeof value === 'string' && isUSVString(value)) || isFileLike(value)) // 5.13. Create an entry with name and value, and append it to entry list. entryList.push(makeEntry(name, value, filename)) @@ -280,7 +297,7 @@ function parseMultipartFormDataHeaders (input, position) { ) // 2.8. Byte-lowercase header name and switch on the result: - switch (new TextDecoder().decode(headerName).toLowerCase()) { + switch (bufferToLowerCasedHeaderName(headerName)) { case 'content-disposition': { // 1. Set name and filename to null. name = filename = null @@ -428,10 +445,9 @@ function parseMultipartFormDataName (input, position) { */ function collectASequenceOfBytes (condition, input, position) { const result = [] - let index = 0 while (position.position < input.length && condition(input[position.position])) { - result[index++] = input[position.position] + result.push(input[position.position]) position.position++ } From f84ec8087e11a26ee3553a0c601f6a73373edae6 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 13 Mar 2024 09:44:06 +0100 Subject: [PATCH 11/11] Bumped v6.8.0 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 087c6aa7bfe..b952ef1b008 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "6.7.1", + "version": "6.8.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": {