Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bru cookie fns #3521

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/bruno-app/src/components/CodeEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ if (!SERVER_RENDERED) {
'bru.sleep(ms)',
'bru.getGlobalEnvVar(key)',
'bru.setGlobalEnvVar(key, value)',
'bru.setCookie(cookieString, url);',
'bru.getCookie(url);',
'bru.runner',
'bru.runner.setNextRequest(requestName)',
'bru.runner.skipRequest()',
Expand Down
3 changes: 2 additions & 1 deletion packages/bruno-cli/src/runner/prepare-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const decomment = require('decomment');
const crypto = require('node:crypto');
const { mergeHeaders, mergeScripts, mergeVars, getTreePathFromCollectionToItem } = require('../utils/collection');
const { createFormData } = require('../utils/form-data');
const { cookieJarWrapper } = require('../utils/cookies');

const prepareRequest = (item = {}, collection = {}) => {
const request = item?.request;
Expand Down Expand Up @@ -188,7 +189,7 @@ const prepareRequest = (item = {}, collection = {}) => {
axiosRequest.collectionVariables = request.collectionVariables;
axiosRequest.folderVariables = request.folderVariables;
axiosRequest.requestVariables = request.requestVariables;

axiosRequest.cookieJar = cookieJarWrapper;
return axiosRequest;
};

Expand Down
50 changes: 49 additions & 1 deletion packages/bruno-cli/src/utils/cookies.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,53 @@ const each = require('lodash/each');

const cookieJar = new CookieJar();

const cookieJarWrapper = () => {
return {
get: function (url, cookieName, callback) {
cookieJar.getCookies(url, (err, cookies) => {
if (err) return callback(err);
const cookie = cookies.find(cookie => cookie.key === cookieName);
callback(null, cookie ? cookie.value : null);
});
},

getSync: function (url) {
const cookies = cookieJar.getCookiesSync(url);
return cookies;
},

getAll: function (url, callback) {
cookieJar.getCookies(url, callback);
},

set: function (url, cookieName, cookieValue, options, callback) {
const cookie = new Cookie({
key: cookieName,
value: cookieValue,
domain: new URL(url).hostname,
path: '/',
...options
});
cookieJar.setCookie(cookie.toString(), url, callback);
},

unset: function (url, cookieName, callback) {
const expiredCookie = new Cookie({
key: cookieName,
value: '',
expires: new Date(0), // Set the cookie to expire in the past
domain: new URL(url).hostname,
path: '/',
});
cookieJar.setCookie(expiredCookie.toString(), url, callback);
},

clear: function (url, callback) {
cookieJar.removeAllCookies(callback);
}
};
}

const addCookieToJar = (setCookieHeader, requestUrl) => {
const cookie = Cookie.parse(setCookieHeader, { loose: true });
cookieJar.setCookieSync(cookie, requestUrl, {
Expand Down Expand Up @@ -96,5 +143,6 @@ module.exports = {
getCookieStringForUrl,
getDomainsWithCookies,
deleteCookiesForDomain,
saveCookies
saveCookies,
cookieJarWrapper
};
65 changes: 37 additions & 28 deletions packages/bruno-electron/src/ipc/network/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-he
const { addDigestInterceptor } = require('./digestauth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
const { chooseFileToSave, writeFile } = require('../../utils/filesystem');
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
const {
resolveOAuth2AuthorizationCodeAccessToken,
transformClientCredentialsRequest,
Expand Down Expand Up @@ -102,6 +102,36 @@ const saveCookies = (url, headers) => {
}
}

const addCookiesToRequest = ({ request }) => {
const cookieString = getCookieStringForUrl(request.url);
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
const existingCookieHeaderName = Object.keys(request.headers).find(
name => name.toLowerCase() === 'cookie'
);
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';

// Helper function to parse cookies into an object
const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
const [name, ...rest] = cookie.split('=');
if (name && name.trim()) {
cookies[name.trim()] = rest.join('=').trim();
}
return cookies;
}, {});

const mergedCookies = {
...parseCookies(existingCookieString),
...parseCookies(cookieString),
};

const combinedCookieString = Object.entries(mergedCookies)
.map(([name, value]) => `${name}=${value}`)
.join('; ');

request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
}
};

const configureRequest = async (
collectionUid,
request,
Expand Down Expand Up @@ -330,33 +360,7 @@ const configureRequest = async (

// add cookies to request
if (preferencesUtil.shouldSendCookies()) {
const cookieString = getCookieStringForUrl(request.url);
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
const existingCookieHeaderName = Object.keys(request.headers).find(
name => name.toLowerCase() === 'cookie'
);
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';

// Helper function to parse cookies into an object
const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
const [name, ...rest] = cookie.split('=');
if (name && name.trim()) {
cookies[name.trim()] = rest.join('=').trim();
}
return cookies;
}, {});

const mergedCookies = {
...parseCookies(existingCookieString),
...parseCookies(cookieString),
};

const combinedCookieString = Object.entries(mergedCookies)
.map(([name, value]) => `${name}=${value}`)
.join('; ');

request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
}
addCookiesToRequest({ request });
}

// Add API key to the URL
Expand Down Expand Up @@ -485,6 +489,11 @@ const registerNetworkIpc = (mainWindow) => {
}
}

// add cookies to request
if (preferencesUtil.shouldSendCookies()) {
addCookiesToRequest({ request });
}

return scriptResult;
};

Expand Down
4 changes: 3 additions & 1 deletion packages/bruno-electron/src/ipc/network/prepare-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ const { get, each, filter, find } = require('lodash');
const decomment = require('decomment');
const crypto = require('node:crypto');
const fs = require('node:fs/promises');
const { cookieJarWrapper } = require('../../utils/cookies');
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection');
const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data');
const { buildFormUrlEncodedPayload } = require('../../utils/form-data');
const path = require('node:path');

const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
Expand Down Expand Up @@ -320,6 +321,7 @@ const prepareRequest = async (item, collection = {}, abortController) => {
axiosRequest.requestVariables = request.requestVariables;
axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables;
axiosRequest.assertions = request.assertions;
axiosRequest.cookieJar = cookieJarWrapper;

return axiosRequest;
};
Expand Down
70 changes: 65 additions & 5 deletions packages/bruno-electron/src/utils/cookies.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,77 @@ const each = require('lodash/each');

const cookieJar = new CookieJar();

const addCookieToJar = (setCookieHeader, requestUrl) => {
const cookieJarWrapper = () => {
return {
get: function (url, cookieName, callback) {
cookieJar.getCookies(url, (err, cookies) => {
if (err) return callback(err);
const cookie = cookies.find(cookie => cookie.key === cookieName);
callback(null, cookie ? cookie.value : null);
});
},

getSync: function (url) {
const cookies = cookieJar.getCookiesSync(url);
return cookies;
},

getAll: function (url, callback) {
cookieJar.getCookies(url, callback);
},

set: function (url, cookieName, cookieValue, options, callback) {
const cookie = new Cookie({
key: cookieName,
value: cookieValue,
domain: new URL(url).hostname,
path: '/',
...options
});
cookieJar.setCookie(cookie.toString(), url, callback);
},

unset: function (url, cookieName, callback) {
const expiredCookie = new Cookie({
key: cookieName,
value: '',
expires: new Date(0), // Set the cookie to expire in the past
domain: new URL(url).hostname,
path: '/',
});
cookieJar.setCookie(expiredCookie.toString(), url, callback);
},

clear: function (url, callback) {
cookieJar.removeAllCookies(callback);
}
};
}

const normalizeUrl = (url) => {
try {
return new URL(url)?.toString?.();
}
catch(error) {
return url;
}
}

const addCookieToJar = (setCookieHeader, _url) => {
const url = normalizeUrl(_url);
const cookie = Cookie.parse(setCookieHeader, { loose: true });
cookieJar.setCookieSync(cookie, requestUrl, {
cookieJar.setCookieSync(cookie, url, {
ignoreError: true // silently ignore things like parse errors and invalid domains
});
};

const getCookiesForUrl = (url) => {
const getCookiesForUrl = (_url) => {
const url = normalizeUrl(_url);
return cookieJar.getCookiesSync(url);
};

const getCookieStringForUrl = (url) => {
const getCookieStringForUrl = (_url) => {
const url = normalizeUrl(_url);
const cookies = getCookiesForUrl(url);

if (!Array.isArray(cookies) || !cookies.length) {
Expand Down Expand Up @@ -81,5 +140,6 @@ module.exports = {
getCookiesForUrl,
getCookieStringForUrl,
getDomainsWithCookies,
deleteCookiesForDomain
deleteCookiesForDomain,
cookieJarWrapper
};
84 changes: 83 additions & 1 deletion packages/bruno-js/src/bru.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { interpolate } = require('@usebruno/common');
const variableNameRegex = /^[\w-.]*$/;

class Bru {
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables) {
constructor(request, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, cookieJar) {
this.envVariables = envVariables || {};
this.runtimeVariables = runtimeVariables || {};
this.processEnvVars = cloneDeep(processEnvVars || {});
Expand All @@ -13,6 +13,7 @@ class Bru {
this.requestVariables = requestVariables || {};
this.globalEnvironmentVariables = globalEnvironmentVariables || {};
this.collectionPath = collectionPath;
this.url = request.url;
this.runner = {
skipRequest: () => {
this.skipRequest = true;
Expand All @@ -23,6 +24,87 @@ class Bru {
setNextRequest: (nextRequest) => {
this.nextRequest = nextRequest;
}
}
this.cookieJar = cookieJar?.();
this.cookies = {
jar: () => ({
get: (url, cookieName, callback = () => {}) => {
const interpolatedUrl = this._interpolate(url);
if (!interpolatedUrl || !cookieName) {
throw new Error("URL and cookie name are required.");
}
return this.cookieJar.get(interpolatedUrl, cookieName, callback);
},
getAll: (url, callback = () => {}) => {
const interpolatedUrl = this._interpolate(url);
if (!interpolatedUrl) {
throw new Error("URL is required.");
}
return this.cookieJar.getAll(interpolatedUrl, callback);
},
set: (url, cookieName, cookieValue, options = {}, callback = () => {}) => {
const interpolatedUrl = this._interpolate(url);
if (!interpolatedUrl || !cookieName || cookieValue === undefined) {
throw new Error("URL, cookie name and value are required.");
}
return this.cookieJar.set(interpolatedUrl, cookieName, cookieValue, options, callback);
},
unset: (url, cookieName, callback = () => {}) => {
const interpolatedUrl = this._interpolate(url);
if (!interpolatedUrl || !cookieName) {
throw new Error("URL and cookie name are required.");
}
this.cookieJar.unset(interpolatedUrl, cookieName, callback);
},
clear: (url, callback = () => {}) => {
const interpolatedUrl = this._interpolate(url);
if (!interpolatedUrl) {
throw new Error("URL is required.");
}
this.cookieJar.clear(interpolatedUrl, callback);
},
has: (url, cookieName, callback = () => {}) => {
const interpolatedUrl = this._interpolate(url);
if (!interpolatedUrl || !cookieName) {
throw new Error("URL and cookie name are required.");
}
this.cookieJar.get(interpolatedUrl, cookieName, (err, cookie) => {
if (err) {
callback(err, false);
} else {
callback(null, !!cookie);
}
});
}
}),
get: (cookieName) => {
const interpolatedUrl = this._interpolate(this.url);
if (!interpolatedUrl || !cookieName) {
throw new Error("URL and cookie name are required.");
}
const cookies = this.cookieJar.getSync(interpolatedUrl);
const cookie = cookies.find(cookie => cookie.key === cookieName);
return cookie ? cookie.value : null;
},
has: (cookieName) => {
const interpolatedUrl = this._interpolate(this.url);
if (!interpolatedUrl || !cookieName) {
throw new Error("URL and cookie name are required.");
}
const cookies = this.cookieJar.getSync(interpolatedUrl);
return cookies.some(cookie => cookie.key === cookieName);
},
toObject: () => {
const interpolatedUrl = this._interpolate(this.url);
if (!interpolatedUrl) {
throw new Error("URL is required.");
}
const cookies = this.cookieJar.getSync(interpolatedUrl);
return cookies.reduce((cookieObj, cookie) => {
cookieObj[cookie.key] = cookie.value;
return cookieObj;
}, {});
}
};
}

Expand Down
Loading