Skip to content
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,7 @@ The first argument can be either a `url` or an `options` object. The only requir
- `baseUrl` - fully qualified uri string used as the base url. Most useful with `request.defaults`, for example when you want to do many requests to the same domain. If `baseUrl` is `https://example.com/api/`, then requesting `/end/point?test=true` will fetch `https://example.com/api/end/point?test=true`. When `baseUrl` is given, `uri` must also be a string.
- `method` - http method (default: `"GET"`)
- `headers` - http headers (default: `{}`)
- `protocolVersion` - HTTP Protocol Version to use. Can be one of `auto|http1|http2` (default: `http1`). Is overridden to `http1` when sending a http request, using proxy, or running in a browser environment.
- `protocolVersion` - HTTP Protocol Version to use. Can be one of `auto|http1|http2` (default: `http1`). Is overridden to `http1` when sending a http request, using proxy, or running in a browser environment.
---

- `qs` - object containing querystring values to be appended to the `uri`
Expand Down Expand Up @@ -1029,7 +1029,7 @@ request.defaults({
tunneling proxy.
- `proxyHeaderExclusiveList` - a whitelist of headers to send
exclusively to a tunneling proxy and not to destination.

- `sslKeyLogFile` - File path to capture SSL session keys
---

- `disableUrlEncoding` - if `true`, it will not use postman-url-encoder to encode URL. It means that if URL is given as object, it will be used as it is without doing any encoding. But if URL is given as string, it will be encoded by Node while converting it to object.
Expand Down
23 changes: 23 additions & 0 deletions request.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
var tls = require('tls')
var http = require('http')
var https = require('https')
var fsPromise = require('fs/promises')
var events = require('events')
var http2 = require('./lib/http2')
var autohttp2 = require('./lib/autohttp')
var url = require('url')
Expand Down Expand Up @@ -805,6 +807,9 @@ Request.prototype.getNewAgent = function ({agentIdleTimeout}) {
if (self.secureOptions) {
options.secureOptions = self.secureOptions
}
if (self.sslKeyLogFile) {
options.sslKeyLogFile = self.sslKeyLogFile
}
if (typeof self.rejectUnauthorized !== 'undefined') {
options.rejectUnauthorized = self.rejectUnauthorized
}
Expand Down Expand Up @@ -905,6 +910,13 @@ Request.prototype.getNewAgent = function ({agentIdleTimeout}) {
}
poolKey += options.secureOptions
}

if (options.sslKeyLogFile) {
if (poolKey) {
poolKey += ':'
}
poolKey += options.sslKeyLogFile
}
}

if (self.pool === globalPool && !poolKey && Object.keys(options).length === 0 && self.httpModule.globalAgent && typeof agentIdleTimeout !== 'number') {
Expand Down Expand Up @@ -1055,6 +1067,17 @@ Request.prototype.start = function () {
}
}

// Attach event only once on the socket, so that data is not written multiple times
// Since the agent key also includes keyLog, we are sure that a socket which is not supposed to be logging the
// ssl content will not have a keylog listener set inadvertently, so we don't need to care about removing this listener
if (self.sslKeyLogFile && !events.getEventListeners(socket, 'keylog').length) {
socket.on('keylog', (line) => {
fsPromise.appendFile(self.sslKeyLogFile, line)
.catch(() => {
debug('Failed to append keylog to file: ' + self.sslKeyLogFile)
})
})
}
// `._connecting` was the old property which was made public in node v6.1.0
var isConnecting = socket._connecting || socket.connecting
if (self.timing) {
Expand Down
2 changes: 2 additions & 0 deletions tests/browser/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ module.exports = function (config) {
debug: true,
configure: function (bundle) {
bundle.require('./tests/browser/socks-proxy-agent-stub.js', { expose: 'socks-proxy-agent' })

bundle.ignore('fs/promises')
},
transform: [istanbul({
ignore: ['**/node_modules/**', '**/tests/**']
Expand Down
6 changes: 4 additions & 2 deletions tests/test-agents.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ var tape = require('tape')
tape('http', function (t) {
var r = request({
uri: 'http://postman-echo.com/get',
followRedirect: false,
agents: {
http: new http.Agent({option1: true})
}
}, function (err, res) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(res.statusCode, 301)
t.ok(r.agent instanceof http.Agent, 'is http.Agent')
t.equal(r.agent.options.option1, true)
t.end()
Expand All @@ -23,6 +24,7 @@ tape('http', function (t) {
tape('http.agentClass + http.agentOptions', function (t) {
var r = request({
uri: 'http://postman-echo.com/get',
followRedirect: false,
agents: {
http: {
agentClass: http.Agent,
Expand All @@ -31,7 +33,7 @@ tape('http.agentClass + http.agentOptions', function (t) {
}
}, function (err, res) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(res.statusCode, 301)
t.ok(r.agent instanceof http.Agent, 'is http.Agent')
t.equal(r.agent.options.option2, true)
t.equal(Object.keys(r.agent.sockets).length, 1, '1 socket name')
Expand Down
101 changes: 101 additions & 0 deletions tests/test-sslKeyLogFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use strict'

var server = require('./server')
var request = require('../index')
var fs = require('fs')
var path = require('path')
var os = require('os')
var tape = require('tape')

var s = server.createSSLServer()
var keylogFilePath = path.join(os.tmpdir(), 'test-keylog-' + Date.now() + '.txt')

tape('setup', function (t) {
s.listen(0, function () {
t.end()
})
})

tape('sslKeyLogFile - file creation and content', function (t) {
// Clean up file if it exists from a previous test run
if (fs.existsSync(keylogFilePath)) {
fs.unlinkSync(keylogFilePath)
}

s.on('/keylogtest', function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('success')
})

request({
url: s.url + '/keylogtest',
rejectUnauthorized: false,
sslKeyLogFile: keylogFilePath
}, function (err, res, body) {
t.equal(err, null, 'request should not error')
t.equal(body, 'success', 'should receive correct response')

// Give a small delay to ensure the keylog file has been written
setTimeout(function () {
// Check if file was created
var fileExists = fs.existsSync(keylogFilePath)
t.ok(fileExists, 'keylog file should be created')

if (fileExists) {
// Check if file contains content
var content = fs.readFileSync(keylogFilePath, 'utf8')
t.ok(content.length > 0, 'keylog file should contain content')
t.ok(content.includes('CLIENT_HANDSHAKE_TRAFFIC_SECRET') ||
content.includes('SERVER_HANDSHAKE_TRAFFIC_SECRET') ||
content.includes('CLIENT_TRAFFIC_SECRET') ||
content.includes('SERVER_TRAFFIC_SECRET'),
'keylog file should contain TLS key material')
}

t.end()
}, 100)
})
})

tape('sslKeyLogFile - multiple requests append to same file', function (t) {
// Use the file from the previous test
var initialSize = 0
if (fs.existsSync(keylogFilePath)) {
initialSize = fs.statSync(keylogFilePath).size
}

s.on('/keylogtest2', function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('success2')
})

request({
url: s.url + '/keylogtest2',
rejectUnauthorized: false,
sslKeyLogFile: keylogFilePath
}, function (err, res, body) {
t.equal(err, null, 'second request should not error')
t.equal(body, 'success2', 'should receive correct response from second request')

setTimeout(function () {
if (fs.existsSync(keylogFilePath)) {
var newSize = fs.statSync(keylogFilePath).size
// The file size should be at least as large as before (might be same if socket is reused)
t.ok(newSize >= initialSize, 'keylog file should have content from multiple requests')
}

t.end()
}, 100)
})
})

tape('cleanup', function (t) {
// Clean up the keylog file
if (fs.existsSync(keylogFilePath)) {
fs.unlinkSync(keylogFilePath)
}

s.close(function () {
t.end()
})
})