diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index 5020f8475..f62f9dc64 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -41,6 +41,7 @@ "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", + "tough-cookie": "^4.1.3", "@usebruno/vm2": "^3.9.13", "xmlbuilder": "^15.1.1", "yargs": "^17.6.2" diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 58b3cdf80..0e1de296b 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -211,6 +211,11 @@ const builder = async (yargs) => { description: 'The specified custom CA certificate (--cacert) will be used exclusively and the default truststore is ignored, if this option is specified. Evaluated in combination with "--cacert" only.' }) + .option('disable-cookies', { + type: 'boolean', + default: false, + description: 'Automatically save and sent cookies with requests' + }) .option('env', { describe: 'Environment variables', type: 'string' @@ -301,6 +306,7 @@ const handler = async function (argv) { filename, cacert, ignoreTruststore, + disableCookies, env, envVar, insecure, @@ -392,6 +398,9 @@ const handler = async function (argv) { if (insecure) { options['insecure'] = true; } + if (disableCookies) { + options['disableCookies'] = true; + } if (cacert && cacert.length) { if (insecure) { console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`)); diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index cfdd8dd8c..62b728560 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -20,6 +20,7 @@ const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-he const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util'); const path = require('path'); const { createFormData } = require('../utils/common'); +const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const onConsoleLog = (type, args) => { @@ -178,6 +179,14 @@ const runSingleRequest = async function ( }); } + //set cookies if enabled + if (!options.disableCookies) { + const cookieString = getCookieStringForUrl(request.url); + if (cookieString && typeof cookieString === 'string' && cookieString.length) { + request.headers['cookie'] = cookieString; + } + } + // stringify the request url encoded params if (request.headers['content-type'] === 'application/x-www-form-urlencoded') { request.data = qs.stringify(request.data); @@ -220,6 +229,11 @@ const runSingleRequest = async function ( // Prevents the duration on leaking to the actual result responseTime = response.headers.get('request-duration'); response.headers.delete('request-duration'); + + //save cookies if enabled + if (!options.disableCookies) { + saveCookies(request.url, response.headers); + } } catch (err) { if (err?.response) { response = err.response; diff --git a/packages/bruno-cli/src/utils/cookies.js b/packages/bruno-cli/src/utils/cookies.js new file mode 100644 index 000000000..acb58b505 --- /dev/null +++ b/packages/bruno-cli/src/utils/cookies.js @@ -0,0 +1,100 @@ +const { Cookie, CookieJar } = require('tough-cookie'); +const each = require('lodash/each'); + +const cookieJar = new CookieJar(); + +const addCookieToJar = (setCookieHeader, requestUrl) => { + const cookie = Cookie.parse(setCookieHeader, { loose: true }); + cookieJar.setCookieSync(cookie, requestUrl, { + ignoreError: true // silently ignore things like parse errors and invalid domains + }); +}; + +const getCookiesForUrl = (url) => { + return cookieJar.getCookiesSync(url); +}; + +const getCookieStringForUrl = (url) => { + const cookies = getCookiesForUrl(url); + + if (!Array.isArray(cookies) || !cookies.length) { + return ''; + } + + const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now()); + + return validCookies.map((cookie) => cookie.cookieString()).join('; '); +}; + +const getDomainsWithCookies = () => { + return new Promise((resolve, reject) => { + const domainCookieMap = {}; + + cookieJar.store.getAllCookies((err, cookies) => { + if (err) { + return reject(err); + } + + cookies.forEach((cookie) => { + if (!domainCookieMap[cookie.domain]) { + domainCookieMap[cookie.domain] = [cookie]; + } else { + domainCookieMap[cookie.domain].push(cookie); + } + }); + + const domains = Object.keys(domainCookieMap); + const domainsWithCookies = []; + + each(domains, (domain) => { + const cookies = domainCookieMap[domain]; + const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now()); + + if (validCookies.length) { + domainsWithCookies.push({ + domain, + cookies: validCookies, + cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ') + }); + } + }); + + resolve(domainsWithCookies); + }); + }); +}; + +const deleteCookiesForDomain = (domain) => { + return new Promise((resolve, reject) => { + cookieJar.store.removeCookies(domain, null, (err) => { + if (err) { + return reject(err); + } + + return resolve(); + }); + }); +}; + +const saveCookies = (url, headers) => { + let setCookieHeaders = []; + if (headers['set-cookie']) { + setCookieHeaders = Array.isArray(headers['set-cookie']) + ? headers['set-cookie'] + : [headers['set-cookie']]; + for (let setCookieHeader of setCookieHeaders) { + if (typeof setCookieHeader === 'string' && setCookieHeader.length) { + addCookieToJar(setCookieHeader, url); + } + } + } +} + +module.exports = { + addCookieToJar, + getCookiesForUrl, + getCookieStringForUrl, + getDomainsWithCookies, + deleteCookiesForDomain, + saveCookies +};