diff --git a/readme.md b/readme.md index dffe717de..1578e351c 100644 --- a/readme.md +++ b/readme.md @@ -192,6 +192,7 @@ This also accepts an `object` with the following fields to constrain the duratio - `lookup` starts when a socket is assigned and ends when the hostname has been resolved. Does not apply when using a Unix domain socket. - `connect` starts when `lookup` completes (or when the socket is assigned if lookup does not apply to the request) and ends when the socket is connected. +- `secureConnect` starts when `connect` completes and ends when the handshaking process completes (HTTPS only). - `socket` starts when the socket is connected. See [request.setTimeout](https://nodejs.org/api/http.html#http_request_settimeout_timeout_callback). - `response` starts when the request has been written to the socket and ends when the response headers are received. - `send` starts when the socket is connected and ends with the request has been written to the socket. diff --git a/source/timed-out.js b/source/timed-out.js index bffb0e2c2..1b5e4aa20 100644 --- a/source/timed-out.js +++ b/source/timed-out.js @@ -104,6 +104,22 @@ module.exports = (request, options) => { }); } + if (delays.secureConnect !== undefined && options.protocol === 'https:') { + request.once('socket', socket => { + if (socket.connecting) { + socket.once('connect', () => { + const cancelTimeout = addTimeout( + delays.secureConnect, + timeoutHandler, + 'secureConnect' + ); + cancelers.push(cancelTimeout); + socket.once('secureConnect', cancelTimeout); + }); + } + }); + } + if (delays.send !== undefined) { request.once('socket', socket => { const timeRequest = () => { diff --git a/test/agent.js b/test/agent.js index 78e3874cc..e43541e1a 100644 --- a/test/agent.js +++ b/test/agent.js @@ -1,8 +1,6 @@ -import util from 'util'; import {Agent as HttpAgent} from 'http'; import {Agent as HttpsAgent} from 'https'; import test from 'ava'; -import pem from 'pem'; import sinon from 'sinon'; import got from '../source'; import {createServer, createSSLServer} from './helpers/server'; @@ -10,34 +8,8 @@ import {createServer, createSSLServer} from './helpers/server'; let http; let https; -const createCertificate = util.promisify(pem.createCertificate); - test.before('setup', async () => { - const caKeys = await createCertificate({ - days: 1, - selfSigned: true - }); - - const caRootKey = caKeys.serviceKey; - const caRootCert = caKeys.certificate; - - const keys = await createCertificate({ - serviceCertificate: caRootCert, - serviceKey: caRootKey, - serial: Date.now(), - days: 500, - country: '', - state: '', - locality: '', - organization: '', - organizationUnit: '', - commonName: 'sindresorhus.com' - }); - - const key = keys.clientKey; - const cert = keys.certificate; - - https = await createSSLServer({key, cert}); // eslint-disable-line object-property-newline + https = await createSSLServer(); http = await createServer(); // HTTPS Handlers diff --git a/test/helpers/server.js b/test/helpers/server.js index e22a8700e..d0876e018 100644 --- a/test/helpers/server.js +++ b/test/helpers/server.js @@ -3,10 +3,13 @@ const util = require('util'); const http = require('http'); const https = require('https'); const getPort = require('get-port'); +const pem = require('pem'); exports.host = 'localhost'; const {host} = exports; +const createCertificate = util.promisify(pem.createCertificate); + exports.createServer = async () => { const port = await getPort(); @@ -25,10 +28,34 @@ exports.createServer = async () => { return s; }; -exports.createSSLServer = async options => { +exports.createSSLServer = async () => { const port = await getPort(); - const s = https.createServer(options, (request, response) => { + const caKeys = await createCertificate({ + days: 1, + selfSigned: true + }); + + const caRootKey = caKeys.serviceKey; + const caRootCert = caKeys.certificate; + + const keys = await createCertificate({ + serviceCertificate: caRootCert, + serviceKey: caRootKey, + serial: Date.now(), + days: 500, + country: '', + state: '', + locality: '', + organization: '', + organizationUnit: '', + commonName: 'sindresorhus.com' + }); + + const key = keys.clientKey; + const cert = keys.certificate; + + const s = https.createServer({cert, key}, (request, response) => { s.emit(request.url, request, response); }); @@ -36,6 +63,7 @@ exports.createSSLServer = async options => { s.port = port; s.url = `https://${host}:${port}`; s.protocol = 'https'; + s.caRootCert = caRootCert; s.listen = util.promisify(s.listen); s.close = util.promisify(s.close); diff --git a/test/https.js b/test/https.js index a02f8745c..fb213740e 100644 --- a/test/https.js +++ b/test/https.js @@ -1,40 +1,11 @@ -import util from 'util'; import test from 'ava'; -import pem from 'pem'; import got from '../source'; import {createSSLServer} from './helpers/server'; let s; -let caRootCert; - -const createCertificate = util.promisify(pem.createCertificate); test.before('setup', async () => { - const caKeys = await createCertificate({ - days: 1, - selfSigned: true - }); - - const caRootKey = caKeys.serviceKey; - caRootCert = caKeys.certificate; - - const keys = await createCertificate({ - serviceCertificate: caRootCert, - serviceKey: caRootKey, - serial: Date.now(), - days: 500, - country: '', - state: '', - locality: '', - organization: '', - organizationUnit: '', - commonName: 'sindresorhus.com' - }); - - const key = keys.clientKey; - const cert = keys.certificate; - - s = await createSSLServer({key, cert}); + s = await createSSLServer(); s.on('/', (request, response) => response.end('ok')); @@ -51,7 +22,7 @@ test('make request to https server without ca', async t => { test('make request to https server with ca', async t => { const {body} = await got(s.url, { - ca: caRootCert, + ca: s.caRootCert, headers: {host: 'sindresorhus.com'} }); t.is(body, 'ok'); @@ -59,7 +30,7 @@ test('make request to https server with ca', async t => { test('protocol-less URLs default to HTTPS', async t => { const {body, requestUrl} = await got(s.url.replace(/^https:\/\//, ''), { - ca: caRootCert, + ca: s.caRootCert, headers: {host: 'sindresorhus.com'} }); t.is(body, 'ok'); diff --git a/test/redirects.js b/test/redirects.js index 7e675c219..0387eabf2 100644 --- a/test/redirects.js +++ b/test/redirects.js @@ -1,41 +1,13 @@ -import util from 'util'; import {URL} from 'url'; import test from 'ava'; -import pem from 'pem'; import got from '../source'; import {createServer, createSSLServer} from './helpers/server'; let http; let https; -const createCertificate = util.promisify(pem.createCertificate); - test.before('setup', async () => { - const caKeys = await createCertificate({ - days: 1, - selfSigned: true - }); - - const caRootKey = caKeys.serviceKey; - const caRootCert = caKeys.certificate; - - const keys = await createCertificate({ - serviceCertificate: caRootCert, - serviceKey: caRootKey, - serial: Date.now(), - days: 500, - country: '', - state: '', - locality: '', - organization: '', - organizationUnit: '', - commonName: 'sindresorhus.com' - }); - - const key = keys.clientKey; - const cert = keys.certificate; - - https = await createSSLServer({key, cert}); + https = await createSSLServer(); http = await createServer(); // HTTPS Handlers diff --git a/test/timeout.js b/test/timeout.js index f1af9edd0..667556bdc 100644 --- a/test/timeout.js +++ b/test/timeout.js @@ -6,9 +6,10 @@ import test from 'ava'; import pEvent from 'p-event'; import delay from 'delay'; import got from '../source'; -import {createServer} from './helpers/server'; +import {createServer, createSSLServer} from './helpers/server'; let s; +let ss; const slowDataStream = () => { const slowStream = new stream.PassThrough(); @@ -36,7 +37,7 @@ const keepAliveAgent = new http.Agent({ }); test.before('setup', async () => { - s = await createServer(); + [s, ss] = await Promise.all([createServer(), createSSLServer()]); s.on('/', async (request, response) => { request.on('data', () => {}); @@ -58,7 +59,11 @@ test.before('setup', async () => { response.end('OK'); }); - await s.listen(s.port); + ss.on('/', async (request, response) => { + response.end('OK'); + }); + + await Promise.all([s.listen(s.port), ss.listen(ss.port)]); }); test('timeout option (ETIMEDOUT)', async t => { @@ -248,6 +253,35 @@ test('connect timeout (ip address)', async t => { ); }); +test('secureConnect timeout', async t => { + await t.throws( + got(ss.url, { + timeout: {secureConnect: 1}, + retry: 0, + rejectUnauthorized: false + }), + { + ...errorMatcher, + message: `Timeout awaiting 'secureConnect' for 1ms` + } + ); +}); + +test('secureConnect timeout not breached', async t => { + const secureConnect = 200; + await got(ss.url, { + timeout: {secureConnect}, + retry: 0, + rejectUnauthorized: false + }).on('request', request => { + request.on('error', error => { + t.fail(`error emitted: ${error}`); + }); + }); + await delay(secureConnect * 2); + t.pass('no error emitted'); +}); + test('lookup timeout', async t => { await t.throws( got({