From d80dfcd0b667e58eceba70beec084773e8e68a90 Mon Sep 17 00:00:00 2001 From: Christian Rose Date: Sat, 9 Mar 2024 13:50:54 +0100 Subject: [PATCH] fix: send correct SNI for proxy connections resolves #2926. With this fix the correct server name indicator (SNI) should be send to a proxy. The SNI extensions allows the proxy to select between different TLS profiles To avoid a loop in the connect method of the client a new error is introduced. --- 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) => {