mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 14:35:03 +00:00
Merge branch 'main' into bugfix/negative-timeout
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "jest",
|
||||
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'github-markdown-css/github-markdown.css';
|
||||
import get from 'lodash/get';
|
||||
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
@@ -1,13 +1,55 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
const proxySchema = Yup.object({
|
||||
use: Yup.string().oneOf(['global', 'true', 'false']),
|
||||
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||
hostname: Yup.string()
|
||||
.when('use', {
|
||||
is: true,
|
||||
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||
otherwise: (hostname) => hostname.nullable()
|
||||
})
|
||||
.max(1024),
|
||||
port: Yup.number()
|
||||
.when('use', {
|
||||
is: true,
|
||||
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
|
||||
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
|
||||
})
|
||||
.min(1)
|
||||
.max(65535),
|
||||
auth: Yup.object()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (username) => username.required('Specify username for proxy authentication.')
|
||||
})
|
||||
.max(1024),
|
||||
password: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (password) => password.required('Specify password for proxy authentication.')
|
||||
})
|
||||
.max(1024)
|
||||
})
|
||||
})
|
||||
.optional(),
|
||||
bypassProxy: Yup.string().optional().max(1024)
|
||||
});
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
enabled: proxyConfig.enabled || false,
|
||||
use: proxyConfig.use || 'global',
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
@@ -15,27 +57,33 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
}
|
||||
},
|
||||
bypassProxy: proxyConfig.bypassProxy || ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
protocol: Yup.string().oneOf(['http', 'https', 'socks5']),
|
||||
hostname: Yup.string().max(1024),
|
||||
port: Yup.number().min(0).max(65535),
|
||||
auth: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string().max(1024),
|
||||
password: Yup.string().max(1024)
|
||||
})
|
||||
}),
|
||||
validationSchema: proxySchema,
|
||||
onSubmit: (values) => {
|
||||
onUpdate(values);
|
||||
proxySchema
|
||||
.validate(values, { abortEarly: true })
|
||||
.then((validatedProxy) => {
|
||||
// serialize 'use' to boolean
|
||||
if (validatedProxy.use === 'true') {
|
||||
validatedProxy.use = true;
|
||||
} else if (validatedProxy.use === 'false') {
|
||||
validatedProxy.use = false;
|
||||
}
|
||||
|
||||
onUpdate(validatedProxy);
|
||||
})
|
||||
.catch((error) => {
|
||||
let errMsg = error.message || 'Preferences validation error';
|
||||
toast.error(errMsg);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues({
|
||||
enabled: proxyConfig.enabled || false,
|
||||
use: proxyConfig.use === true ? 'true' : proxyConfig.use === false ? 'false' : 'global',
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
@@ -43,18 +91,66 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
}
|
||||
},
|
||||
bypassProxy: proxyConfig.bypassProxy || ''
|
||||
});
|
||||
}, [proxyConfig]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<h1 className="font-medium mb-3">Proxy Settings</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="enabled">
|
||||
Enabled
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Config
|
||||
<Tooltip
|
||||
text={`
|
||||
<div>
|
||||
<ul>
|
||||
<li><span style="width: 50px;display:inline-block;">global</span> - use global proxy config</li>
|
||||
<li><span style="width: 50px;display:inline-block;">enabled</span> - use collection proxy config</li>
|
||||
<li><span style="width: 50px;display:inline-block;">disable</span> - disable proxy</li>
|
||||
</ul>
|
||||
</div>
|
||||
`}
|
||||
tooltipId="request-var"
|
||||
/>
|
||||
</label>
|
||||
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="use"
|
||||
value="global"
|
||||
checked={formik.values.use === 'global'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
global
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="use"
|
||||
value={'true'}
|
||||
checked={formik.values.use === 'true'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
enabled
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="use"
|
||||
value={'false'}
|
||||
checked={formik.values.use === 'false'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
@@ -83,6 +179,17 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
/>
|
||||
https
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={formik.values.protocol === 'socks4'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
socks4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
@@ -113,7 +220,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
value={formik.values.hostname || ''}
|
||||
/>
|
||||
{formik.touched.hostname && formik.errors.hostname ? (
|
||||
<div className="text-red-500">{formik.errors.hostname}</div>
|
||||
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
@@ -132,7 +239,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.port}
|
||||
/>
|
||||
{formik.touched.port && formik.errors.port ? <div className="text-red-500">{formik.errors.port}</div> : null}
|
||||
{formik.touched.port && formik.errors.port ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.port}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
@@ -163,7 +272,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||
<div className="text-red-500">{formik.errors.auth.username}</div>
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
@@ -183,10 +292,30 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||
<div className="text-red-500">{formik.errors.auth.password}</div>
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="bypassProxy">
|
||||
Proxy Bypass
|
||||
</label>
|
||||
<input
|
||||
id="bypassProxy"
|
||||
type="text"
|
||||
name="bypassProxy"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.bypassProxy || ''}
|
||||
/>
|
||||
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
|
||||
@@ -36,7 +36,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
brunoConfig.proxy = config;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully');
|
||||
toast.success('Collection settings updated successfully.');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { toastError } from 'utils/common/error';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment';
|
||||
import { IconUpload } from '@tabler/icons';
|
||||
import { IconDownload } from '@tabler/icons';
|
||||
import ImportEnvironment from '../ImportEnvironment';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -73,7 +73,7 @@ const EnvironmentList = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex items-center btn-import-environment" onClick={() => setOpenImportModal(true)}>
|
||||
<IconUpload size={12} strokeWidth={2} />
|
||||
<IconDownload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Import</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ const General = ({ close }) => {
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="flex items-center mt-2">
|
||||
<label className="block font-medium mr-2 select-none" style={{ minWidth: 200 }} htmlFor="sslVerification">
|
||||
SSL Certificate Verification
|
||||
SSL/TLS Certificate Verification
|
||||
</label>
|
||||
<input
|
||||
id="ssl-cert-verification"
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.settings-label {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.textbox {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.15rem 0.45rem;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,292 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
const ProxySettings = ({ close }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const proxySchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||
hostname: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||
otherwise: (hostname) => hostname.nullable()
|
||||
})
|
||||
.max(1024),
|
||||
port: Yup.number()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
|
||||
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
|
||||
})
|
||||
.min(1)
|
||||
.max(65535),
|
||||
auth: Yup.object()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string()
|
||||
.when(['enabled'], {
|
||||
is: true,
|
||||
then: (username) => username.required('Specify username for proxy authentication.')
|
||||
})
|
||||
.max(1024),
|
||||
password: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (password) => password.required('Specify password for proxy authentication.')
|
||||
})
|
||||
.max(1024)
|
||||
})
|
||||
})
|
||||
.optional(),
|
||||
bypassProxy: Yup.string().optional().max(1024)
|
||||
});
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
enabled: preferences.proxy.enabled || false,
|
||||
protocol: preferences.proxy.protocol || 'http',
|
||||
hostname: preferences.proxy.hostname || '',
|
||||
port: preferences.proxy.port || 0,
|
||||
auth: {
|
||||
enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false,
|
||||
username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '',
|
||||
password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : ''
|
||||
},
|
||||
bypassProxy: preferences.proxy.bypassProxy || ''
|
||||
},
|
||||
validationSchema: proxySchema,
|
||||
onSubmit: (values) => {
|
||||
onUpdate(values);
|
||||
}
|
||||
});
|
||||
|
||||
const onUpdate = (values) => {
|
||||
proxySchema
|
||||
.validate(values, { abortEarly: true })
|
||||
.then((validatedProxy) => {
|
||||
dispatch(
|
||||
savePreferences({
|
||||
...preferences,
|
||||
proxy: validatedProxy
|
||||
})
|
||||
).then(() => {
|
||||
close();
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
let errMsg = error.message || 'Preferences validation error';
|
||||
toast.error(errMsg);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues({
|
||||
enabled: preferences.proxy.enabled || false,
|
||||
protocol: preferences.proxy.protocol || 'http',
|
||||
hostname: preferences.proxy.hostname || '',
|
||||
port: preferences.proxy.port || '',
|
||||
auth: {
|
||||
enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false,
|
||||
username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '',
|
||||
password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : ''
|
||||
},
|
||||
bypassProxy: preferences.proxy.bypassProxy || ''
|
||||
});
|
||||
}, [preferences]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<h1 className="font-medium mb-3">Global Proxy Settings</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="enabled">
|
||||
Enabled
|
||||
</label>
|
||||
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
Protocol
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={formik.values.protocol === 'http'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
http
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={formik.values.protocol === 'https'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
https
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={formik.values.protocol === 'socks4'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
socks4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={formik.values.protocol === 'socks5'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
socks5
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
<input
|
||||
id="hostname"
|
||||
type="text"
|
||||
name="hostname"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.hostname || ''}
|
||||
/>
|
||||
{formik.touched.hostname && formik.errors.hostname ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
name="port"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.port}
|
||||
/>
|
||||
{formik.touched.port && formik.errors.port ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.port}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
Auth
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.enabled"
|
||||
checked={formik.values.auth.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="auth.username"
|
||||
type="text"
|
||||
name="auth.username"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.username}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="auth.password"
|
||||
type="text"
|
||||
name="auth.password"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.password}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="bypassProxy">
|
||||
Proxy Bypass
|
||||
</label>
|
||||
<input
|
||||
id="bypassProxy"
|
||||
type="text"
|
||||
name="bypassProxy"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.bypassProxy || ''}
|
||||
/>
|
||||
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-md btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProxySettings;
|
||||
@@ -5,6 +5,7 @@ import Support from './Support';
|
||||
import General from './General';
|
||||
import Font from './Font';
|
||||
import Theme from './Theme';
|
||||
import Proxy from './ProxySettings';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Preferences = ({ onClose }) => {
|
||||
@@ -22,6 +23,10 @@ const Preferences = ({ onClose }) => {
|
||||
return <General close={onClose} />;
|
||||
}
|
||||
|
||||
case 'proxy': {
|
||||
return <Proxy close={onClose} />;
|
||||
}
|
||||
|
||||
case 'theme': {
|
||||
return <Theme close={onClose} />;
|
||||
}
|
||||
@@ -49,6 +54,9 @@ const Preferences = ({ onClose }) => {
|
||||
<div className={getTabClassname('font')} role="tab" onClick={() => setTab('font')}>
|
||||
Font
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
</div>
|
||||
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
||||
Support
|
||||
</div>
|
||||
|
||||
@@ -36,12 +36,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
|
||||
let {
|
||||
schema,
|
||||
loadSchema,
|
||||
isLoading: isSchemaLoading,
|
||||
error: schemaError
|
||||
} = useGraphqlSchema(url, environment, request, collection);
|
||||
let { schema, loadSchema, isLoading: isSchemaLoading } = useGraphqlSchema(url, environment, request, collection);
|
||||
|
||||
const loadGqlSchema = () => {
|
||||
if (!isSchemaLoading) {
|
||||
|
||||
@@ -26,7 +26,12 @@ const useGraphqlSchema = (endpoint, environment, request, collection) => {
|
||||
const loadSchema = () => {
|
||||
setIsLoading(true);
|
||||
fetchGqlSchema(endpoint, environment, request, collection)
|
||||
.then((res) => res.data)
|
||||
.then((res) => {
|
||||
if (!res || res.status !== 200) {
|
||||
return Promise.reject(new Error(res.statusText));
|
||||
}
|
||||
return res.data;
|
||||
})
|
||||
.then((s) => {
|
||||
if (s && s.data) {
|
||||
setSchema(buildClientSchema(s.data));
|
||||
@@ -40,7 +45,7 @@ const useGraphqlSchema = (endpoint, environment, request, collection) => {
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setError(err);
|
||||
toast.error('Error occurred while loading GraphQL Schema');
|
||||
toast.error(`Error occurred while loading GraphQL Schema: ${err.message}`);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.body-mode-selector {
|
||||
background: ${(props) => props.theme.requestTabPanel.bodyModeSelect.color};
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
@@ -15,6 +15,10 @@ const Wrapper = styled.div`
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
|
||||
@@ -6,16 +6,19 @@ import { useDispatch } from 'react-redux';
|
||||
import { updateRequestBodyMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { toastError } from 'utils/common/error';
|
||||
|
||||
const RequestBodyMode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const bodyMode = body?.mode;
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none">
|
||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
|
||||
{humanizeRequestBodyMode(bodyMode)} <IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
@@ -31,6 +34,24 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onPrettify = () => {
|
||||
if (body?.json && bodyMode === 'json') {
|
||||
try {
|
||||
const bodyJson = JSON.parse(body.json);
|
||||
const prettyBodyJson = JSON.stringify(bodyJson, null, 2);
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: prettyBodyJson,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastError(new Error('Unable to prettify. Invalid JSON format.'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer body-mode-selector">
|
||||
@@ -103,6 +124,11 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{bodyMode === 'json' && (
|
||||
<button className="ml-1" onClick={onPrettify}>
|
||||
Prettify
|
||||
</button>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,10 @@ const ResponseSize = ({ size }) => {
|
||||
sizeToDisplay = size + 'B';
|
||||
}
|
||||
|
||||
return <StyledWrapper className="ml-4">{sizeToDisplay}</StyledWrapper>;
|
||||
return (
|
||||
<StyledWrapper title={size.toLocaleString() + 'B'} className="ml-4">
|
||||
{sizeToDisplay}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default ResponseSize;
|
||||
|
||||
@@ -88,44 +88,41 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
});
|
||||
|
||||
const handleClick = (event) => {
|
||||
switch (event.button) {
|
||||
case 0: // left click
|
||||
if (isItemARequest(item)) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
uid: item.uid
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isItemARequest(item)) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
collectionFolderClicked({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
focusTab({
|
||||
uid: item.uid
|
||||
})
|
||||
);
|
||||
return;
|
||||
case 2: // right click
|
||||
const _menuDropdown = dropdownTippyRef.current;
|
||||
if (_menuDropdown) {
|
||||
let menuDropdownBehavior = 'show';
|
||||
if (_menuDropdown.state.isShown) {
|
||||
menuDropdownBehavior = 'hide';
|
||||
}
|
||||
_menuDropdown[menuDropdownBehavior]();
|
||||
}
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
collectionFolderClicked({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const _menuDropdown = dropdownTippyRef.current;
|
||||
if (_menuDropdown) {
|
||||
let menuDropdownBehavior = 'show';
|
||||
if (_menuDropdown.state.isShown) {
|
||||
menuDropdownBehavior = 'hide';
|
||||
}
|
||||
_menuDropdown[menuDropdownBehavior]();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -203,7 +200,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
? indents.map((i) => {
|
||||
return (
|
||||
<div
|
||||
onMouseUp={handleClick}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="indent-block"
|
||||
key={i}
|
||||
@@ -219,7 +217,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
})
|
||||
: null}
|
||||
<div
|
||||
onMouseUp={handleClick}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="flex flex-grow items-center h-full overflow-hidden"
|
||||
style={{
|
||||
|
||||
@@ -18,7 +18,7 @@ const RemoveCollection = ({ onClose, collection }) => {
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Remove Collection" confirmText="Remove" handleConfirm={onConfirm} handleCancel={onClose}>
|
||||
Are you sure you want to remove this collection?
|
||||
Are you sure you want to delete collection <span className="font-semibold">{collection.name}</span> ?
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,20 +66,17 @@ const Collection = ({ collection, searchText }) => {
|
||||
});
|
||||
|
||||
const handleClick = (event) => {
|
||||
dispatch(collectionClicked(collection.uid));
|
||||
};
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const _menuDropdown = menuDropdownTippyRef.current;
|
||||
switch (event.button) {
|
||||
case 0: // left click
|
||||
dispatch(collectionClicked(collection.uid));
|
||||
return;
|
||||
case 2: // right click
|
||||
if (_menuDropdown) {
|
||||
let menuDropdownBehavior = 'show';
|
||||
if (_menuDropdown.state.isShown) {
|
||||
menuDropdownBehavior = 'hide';
|
||||
}
|
||||
_menuDropdown[menuDropdownBehavior]();
|
||||
}
|
||||
return;
|
||||
if (_menuDropdown) {
|
||||
let menuDropdownBehavior = 'show';
|
||||
if (_menuDropdown.state.isShown) {
|
||||
menuDropdownBehavior = 'hide';
|
||||
}
|
||||
_menuDropdown[menuDropdownBehavior]();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,7 +135,11 @@ const Collection = ({ collection, searchText }) => {
|
||||
<CollectionProperties collection={collection} onClose={() => setCollectionPropertiesModal(false)} />
|
||||
)}
|
||||
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||
<div className="flex flex-grow items-center overflow-hidden" onMouseUp={handleClick}>
|
||||
<div
|
||||
className="flex flex-grow items-center overflow-hidden"
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
>
|
||||
<IconChevronRight
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
|
||||
@@ -76,7 +76,14 @@ const CreateCollection = ({ onClose }) => {
|
||||
name="collectionName"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={formik.handleChange}
|
||||
onChange = {
|
||||
(e) => {
|
||||
formik.handleChange(e);
|
||||
if (formik.values.collectionName === formik.values.collectionFolderName) {
|
||||
formik.setFieldValue("collectionFolderName", e.target.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
|
||||
@@ -18,7 +18,7 @@ const Tooltip = ({ text, tooltipId }) => {
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
|
||||
</svg>
|
||||
<ReactTooltip anchorId={tooltipId} content={text} />
|
||||
<ReactTooltip anchorId={tooltipId} html={text} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconBrandGithub, IconPlus, IconUpload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
|
||||
import { IconBrandGithub, IconPlus, IconDownload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
@@ -69,7 +69,7 @@ const Welcome = () => {
|
||||
<span className="label ml-2">Open Collection</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
|
||||
<IconUpload size={18} strokeWidth={2} />
|
||||
<IconDownload size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="import-collection">
|
||||
Import Collection
|
||||
</span>
|
||||
|
||||
@@ -45,6 +45,8 @@ export const fetchGqlSchema = async (endpoint, environment, request, collection)
|
||||
|
||||
export const cancelNetworkRequest = async (cancelTokenUid) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('cancel-http-request', cancelTokenUid).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -162,9 +162,7 @@ const getCollectionRoot = (dir) => {
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(collectionRootPath, 'utf8');
|
||||
const json = collectionBruToJson(content);
|
||||
|
||||
return json;
|
||||
return collectionBruToJson(content);
|
||||
};
|
||||
|
||||
const builder = async (yargs) => {
|
||||
|
||||
@@ -88,6 +88,13 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
axiosRequest.data = request.body.xml;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'sparql') {
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'application/sparql-query';
|
||||
}
|
||||
axiosRequest.data = request.body.sparql;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'formUrlEncoded') {
|
||||
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
const params = {};
|
||||
|
||||
@@ -16,6 +16,7 @@ const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { HttpProxyAgent } = require('http-proxy-agent');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
const { makeAxiosInstance } = require('../utils/axios-instance');
|
||||
const { shouldUseProxy } = require('../utils/proxy-util');
|
||||
|
||||
const runSingleRequest = async function (
|
||||
filename,
|
||||
@@ -47,7 +48,7 @@ const runSingleRequest = async function (
|
||||
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(bruJson, 'request.vars.req');
|
||||
if (preRequestVars && preRequestVars.length) {
|
||||
if (preRequestVars?.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runPreRequestVars(
|
||||
preRequestVars,
|
||||
@@ -64,7 +65,7 @@ const runSingleRequest = async function (
|
||||
get(collectionRoot, 'request.script.req'),
|
||||
get(bruJson, 'request.script.req')
|
||||
]).join(os.EOL);
|
||||
if (requestScriptFile && requestScriptFile.length) {
|
||||
if (requestScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runRequestScript(
|
||||
decomment(requestScriptFile),
|
||||
@@ -87,36 +88,56 @@ const runSingleRequest = async function (
|
||||
if (insecure) {
|
||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
||||
} else {
|
||||
const cacertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
|
||||
const cacert = cacertArray.find((el) => el);
|
||||
if (cacert && cacert.length > 1) {
|
||||
const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
|
||||
const caCert = caCertArray.find((el) => el);
|
||||
if (caCert && caCert.length > 1) {
|
||||
try {
|
||||
caCrt = fs.readFileSync(cacert);
|
||||
httpsAgentRequestFields['ca'] = caCrt;
|
||||
httpsAgentRequestFields['ca'] = fs.readFileSync(caCert);
|
||||
} catch (err) {
|
||||
console.log('Error reading CA cert file:' + cacert, err);
|
||||
console.log('Error reading CA cert file:' + caCert, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const interpolationOptions = {
|
||||
envVars: envVariables,
|
||||
collectionVariables,
|
||||
processEnvVars
|
||||
};
|
||||
|
||||
// client certificate config
|
||||
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
|
||||
for (let clientCert of clientCertConfig) {
|
||||
const domain = interpolateString(clientCert.domain, interpolationOptions);
|
||||
const certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
|
||||
const keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
|
||||
if (domain && certFilePath && keyFilePath) {
|
||||
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
|
||||
|
||||
if (request.url.match(hostRegex)) {
|
||||
try {
|
||||
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
|
||||
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
|
||||
} catch (err) {
|
||||
console.log('Error reading cert/key file', err);
|
||||
}
|
||||
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set proxy if enabled
|
||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
||||
if (proxyEnabled) {
|
||||
let proxyUri;
|
||||
const interpolationOptions = {
|
||||
envVars: envVariables,
|
||||
collectionVariables,
|
||||
processEnvVars
|
||||
};
|
||||
|
||||
const shouldProxy = shouldUseProxy(request.url, get(brunoConfig, 'proxy.bypassProxy', ''));
|
||||
if (proxyEnabled && shouldProxy) {
|
||||
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
|
||||
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
|
||||
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
|
||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
|
||||
interpolateString;
|
||||
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
|
||||
@@ -128,16 +149,13 @@ const runSingleRequest = async function (
|
||||
|
||||
if (socksEnabled) {
|
||||
const socksProxyAgent = new SocksProxyAgent(proxyUri);
|
||||
|
||||
request.httpsAgent = socksProxyAgent;
|
||||
|
||||
request.httpAgent = socksProxyAgent;
|
||||
} else {
|
||||
request.httpsAgent = new HttpsProxyAgent(
|
||||
proxyUri,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
|
||||
request.httpAgent = new HttpProxyAgent(proxyUri);
|
||||
}
|
||||
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
@@ -163,7 +181,7 @@ const runSingleRequest = async function (
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
} catch (err) {
|
||||
if (err && err.response) {
|
||||
if (err?.response) {
|
||||
response = err.response;
|
||||
|
||||
// Prevents the duration on leaking to the actual result
|
||||
@@ -199,7 +217,7 @@ const runSingleRequest = async function (
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||
if (postResponseVars && postResponseVars.length) {
|
||||
if (postResponseVars?.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
varsRuntime.runPostResponseVars(
|
||||
postResponseVars,
|
||||
@@ -217,7 +235,7 @@ const runSingleRequest = async function (
|
||||
get(collectionRoot, 'request.script.res'),
|
||||
get(bruJson, 'request.script.res')
|
||||
]).join(os.EOL);
|
||||
if (responseScriptFile && responseScriptFile.length) {
|
||||
if (responseScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runResponseScript(
|
||||
decomment(responseScriptFile),
|
||||
@@ -275,7 +293,7 @@ const runSingleRequest = async function (
|
||||
testResults = get(result, 'results', []);
|
||||
}
|
||||
|
||||
if (testResults && testResults.length) {
|
||||
if (testResults?.length) {
|
||||
each(testResults, (testResult) => {
|
||||
if (testResult.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
||||
|
||||
@@ -4,10 +4,10 @@ const axios = require('axios');
|
||||
* Function that configures axios with timing interceptors
|
||||
* Important to note here that the timings are not completely accurate.
|
||||
* @see https://github.com/axios/axios/issues/695
|
||||
* @returns {import('axios').AxiosStatic}
|
||||
* @returns {axios.AxiosInstance}
|
||||
*/
|
||||
function makeAxiosInstance() {
|
||||
/** @type {import('axios').AxiosStatic} */
|
||||
/** @type {axios.AxiosInstance} */
|
||||
const instance = axios.create();
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
@@ -26,9 +26,7 @@ function makeAxiosInstance() {
|
||||
if (error.response) {
|
||||
const end = Date.now();
|
||||
const start = error.config.headers['request-start-time'];
|
||||
if (error.response) {
|
||||
error.response.headers['request-duration'] = end - start;
|
||||
}
|
||||
error.response.headers['request-duration'] = end - start;
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
66
packages/bruno-cli/src/utils/proxy-util.js
Normal file
66
packages/bruno-cli/src/utils/proxy-util.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const parseUrl = require('url').parse;
|
||||
|
||||
const DEFAULT_PORTS = {
|
||||
ftp: 21,
|
||||
gopher: 70,
|
||||
http: 80,
|
||||
https: 443,
|
||||
ws: 80,
|
||||
wss: 443
|
||||
};
|
||||
/**
|
||||
* check for proxy bypass, Copied form 'proxy-from-env'
|
||||
*/
|
||||
const shouldUseProxy = (url, proxyBypass) => {
|
||||
if (proxyBypass === '*') {
|
||||
return false; // Never proxy if wildcard is set.
|
||||
}
|
||||
|
||||
// use proxy if no proxyBypass is set
|
||||
if (!proxyBypass || typeof proxyBypass !== 'string' || isEmpty(proxyBypass.trim())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {};
|
||||
let proto = parsedUrl.protocol;
|
||||
let hostname = parsedUrl.host;
|
||||
let port = parsedUrl.port;
|
||||
if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') {
|
||||
return false; // Don't proxy URLs without a valid scheme or host.
|
||||
}
|
||||
|
||||
proto = proto.split(':', 1)[0];
|
||||
// Stripping ports in this way instead of using parsedUrl.hostname to make
|
||||
// sure that the brackets around IPv6 addresses are kept.
|
||||
hostname = hostname.replace(/:\d*$/, '');
|
||||
port = parseInt(port) || DEFAULT_PORTS[proto] || 0;
|
||||
|
||||
return proxyBypass.split(/[,;\s]/).every(function (dontProxyFor) {
|
||||
if (!dontProxyFor) {
|
||||
return true; // Skip zero-length hosts.
|
||||
}
|
||||
|
||||
const parsedProxy = dontProxyFor.match(/^(.+):(\d+)$/);
|
||||
let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor;
|
||||
const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0;
|
||||
if (parsedProxyPort && parsedProxyPort !== port) {
|
||||
return true; // Skip if ports don't match.
|
||||
}
|
||||
|
||||
if (!/^[.*]/.test(parsedProxyHostname)) {
|
||||
// No wildcards, so stop proxying if there is an exact match.
|
||||
return hostname !== parsedProxyHostname;
|
||||
}
|
||||
|
||||
if (parsedProxyHostname.charAt(0) === '*') {
|
||||
// Remove leading wildcard.
|
||||
parsedProxyHostname = parsedProxyHostname.slice(1);
|
||||
}
|
||||
// Stop proxying if the hostname ends with the no_proxy host.
|
||||
return !hostname.endsWith(parsedProxyHostname);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
shouldUseProxy
|
||||
};
|
||||
@@ -44,7 +44,7 @@ const template = [
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [{ role: 'minimize' }, { role: 'close' }]
|
||||
submenu: [{ role: 'minimize' }, { role: 'close', accelerator: 'CommandOrControl+Shift+Q' }]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
|
||||
@@ -75,7 +75,7 @@ app.on('ready', async () => {
|
||||
});
|
||||
|
||||
// register all ipc handlers
|
||||
registerNetworkIpc(mainWindow, watcher, lastOpenedCollections);
|
||||
registerNetworkIpc(mainWindow);
|
||||
registerCollectionsIpc(mainWindow, watcher, lastOpenedCollections);
|
||||
registerPreferencesIpc(mainWindow, watcher, lastOpenedCollections);
|
||||
});
|
||||
|
||||
@@ -32,9 +32,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// browse directory
|
||||
ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => {
|
||||
try {
|
||||
const dirPath = await browseDirectory(mainWindow);
|
||||
|
||||
return dirPath;
|
||||
return await browseDirectory(mainWindow);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -67,8 +65,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
||||
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -93,8 +89,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
collectionPathname,
|
||||
newName
|
||||
});
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -311,7 +305,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
fs.unlinkSync(pathname);
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
return Promise.reject();
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -5,7 +5,7 @@ function isStrPresent(str) {
|
||||
return str && str !== '' && str !== 'undefined';
|
||||
}
|
||||
|
||||
async function resolveCredentials(request) {
|
||||
async function resolveAwsV4Credentials(request) {
|
||||
const awsv4 = request.awsv4config;
|
||||
if (isStrPresent(awsv4.profileName)) {
|
||||
try {
|
||||
@@ -52,5 +52,5 @@ function addAwsV4Interceptor(axiosInstance, request) {
|
||||
|
||||
module.exports = {
|
||||
addAwsV4Interceptor,
|
||||
resolveCredentials
|
||||
resolveAwsV4Credentials
|
||||
};
|
||||
|
||||
@@ -4,10 +4,10 @@ const axios = require('axios');
|
||||
* Function that configures axios with timing interceptors
|
||||
* Important to note here that the timings are not completely accurate.
|
||||
* @see https://github.com/axios/axios/issues/695
|
||||
* @returns {import('axios').AxiosStatic}
|
||||
* @returns {axios.AxiosInstance}
|
||||
*/
|
||||
function makeAxiosInstance() {
|
||||
/** @type {import('axios').AxiosStatic} */
|
||||
/** @type {axios.AxiosInstance} */
|
||||
const instance = axios.create();
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
|
||||
@@ -16,14 +16,15 @@ const { uuid } = require('../../utils/common');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
|
||||
const { getPreferences } = require('../../store/preferences');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { HttpProxyAgent } = require('http-proxy-agent');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
const { makeAxiosInstance } = require('./axios-instance');
|
||||
const { addAwsV4Interceptor, resolveCredentials } = require('./awsv4auth-helper');
|
||||
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
|
||||
const { shouldUseProxy } = require('../../utils/proxy-util');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
@@ -83,6 +84,96 @@ const getSize = (data) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
const configureRequest = async (collectionUid, request, envVars, collectionVariables, processEnvVars) => {
|
||||
const httpsAgentRequestFields = {};
|
||||
if (!preferencesUtil.shouldVerifyTls()) {
|
||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
||||
}
|
||||
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const interpolationOptions = {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
processEnvVars
|
||||
};
|
||||
|
||||
// client certificate config
|
||||
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
|
||||
for (let clientCert of clientCertConfig) {
|
||||
const domain = interpolateString(clientCert.domain, interpolationOptions);
|
||||
const certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
|
||||
const keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
|
||||
if (domain && certFilePath && keyFilePath) {
|
||||
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
|
||||
|
||||
if (request.url.match(hostRegex)) {
|
||||
try {
|
||||
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
|
||||
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
|
||||
} catch (err) {
|
||||
console.log('Error reading cert/key file', err);
|
||||
}
|
||||
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// proxy configuration
|
||||
let proxyConfig = get(brunoConfig, 'proxy', {});
|
||||
let proxyEnabled = get(proxyConfig, 'enabled', 'disabled');
|
||||
if (proxyEnabled === 'global') {
|
||||
proxyConfig = preferencesUtil.getGlobalProxyConfig();
|
||||
proxyEnabled = get(proxyConfig, 'enabled', false);
|
||||
}
|
||||
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));
|
||||
if (proxyEnabled === true && shouldProxy) {
|
||||
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
|
||||
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
|
||||
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
|
||||
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
|
||||
}
|
||||
|
||||
if (socksEnabled) {
|
||||
const socksProxyAgent = new SocksProxyAgent(proxyUri);
|
||||
request.httpsAgent = socksProxyAgent;
|
||||
request.httpAgent = socksProxyAgent;
|
||||
} else {
|
||||
request.httpsAgent = new HttpsProxyAgent(
|
||||
proxyUri,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
request.httpAgent = new HttpProxyAgent(proxyUri);
|
||||
}
|
||||
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
|
||||
if (request.awsv4config) {
|
||||
request.awsv4config = await resolveAwsV4Credentials(request);
|
||||
addAwsV4Interceptor(axiosInstance, request);
|
||||
delete request.awsv4config;
|
||||
}
|
||||
|
||||
request.timeout = preferencesUtil.getRequestTimeout();
|
||||
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
const registerNetworkIpc = (mainWindow) => {
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
|
||||
@@ -134,7 +225,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(request, 'vars.req', []);
|
||||
if (preRequestVars && preRequestVars.length) {
|
||||
if (preRequestVars?.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
const result = varsRuntime.runPreRequestVars(
|
||||
preRequestVars,
|
||||
@@ -155,15 +246,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
const preferences = getPreferences();
|
||||
const timeout = get(preferences, 'request.timeout', 0);
|
||||
request.timeout = timeout;
|
||||
|
||||
// run pre-request script
|
||||
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
|
||||
os.EOL
|
||||
);
|
||||
if (requestScript && requestScript.length) {
|
||||
if (requestScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = await scriptRuntime.runRequestScript(
|
||||
decomment(requestScript),
|
||||
@@ -209,96 +296,49 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
const sslVerification = get(preferences, 'request.sslVerification', true);
|
||||
const httpsAgentRequestFields = {};
|
||||
if (!sslVerification) {
|
||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
||||
}
|
||||
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const interpolationOptions = {
|
||||
const axiosInstance = await configureRequest(
|
||||
collectionUid,
|
||||
request,
|
||||
envVars,
|
||||
collectionVariables,
|
||||
processEnvVars
|
||||
};
|
||||
);
|
||||
|
||||
// client certificate config
|
||||
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
|
||||
let response, responseTime;
|
||||
try {
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
|
||||
for (clientCert of clientCertConfig) {
|
||||
const domain = interpolateString(clientCert.domain, interpolationOptions);
|
||||
const certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
|
||||
const keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
|
||||
if (domain && certFilePath && keyFilePath) {
|
||||
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
} catch (error) {
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
|
||||
if (request.url.match(hostRegex)) {
|
||||
try {
|
||||
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
|
||||
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
|
||||
} catch (err) {
|
||||
console.log('Error reading cert/key file', err);
|
||||
}
|
||||
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
|
||||
break;
|
||||
}
|
||||
// if it's a cancel request, don't continue
|
||||
if (axios.isCancel(error)) {
|
||||
let error = new Error('Request cancelled');
|
||||
error.isCancel = true;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// proxy configuration
|
||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
||||
if (proxyEnabled) {
|
||||
let proxyUri;
|
||||
if (error?.response) {
|
||||
response = error.response;
|
||||
|
||||
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
|
||||
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
|
||||
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
|
||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
|
||||
// if it's not a network error, don't continue
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (socksEnabled) {
|
||||
const socksProxyAgent = new SocksProxyAgent(proxyUri);
|
||||
|
||||
request.httpsAgent = socksProxyAgent;
|
||||
|
||||
request.httpAgent = socksProxyAgent;
|
||||
} else {
|
||||
request.httpsAgent = new HttpsProxyAgent(
|
||||
proxyUri,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
|
||||
request.httpAgent = new HttpProxyAgent(proxyUri);
|
||||
}
|
||||
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
|
||||
if (request.awsv4config) {
|
||||
request.awsv4config = await resolveCredentials(request);
|
||||
addAwsV4Interceptor(axiosInstance, request);
|
||||
delete request.awsv4config;
|
||||
}
|
||||
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
const response = await axiosInstance(request);
|
||||
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(request, 'vars.res', []);
|
||||
if (postResponseVars && postResponseVars.length) {
|
||||
if (postResponseVars?.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
const result = varsRuntime.runPostResponseVars(
|
||||
postResponseVars,
|
||||
@@ -324,7 +364,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const responseScript = compact([get(collectionRoot, 'request.script.res'), get(request, 'script.res')]).join(
|
||||
os.EOL
|
||||
);
|
||||
if (responseScript && responseScript.length) {
|
||||
if (responseScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = await scriptRuntime.runResponseScript(
|
||||
decomment(responseScript),
|
||||
@@ -403,100 +443,16 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
}
|
||||
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
// Prevents the duration on leaking to the actual result
|
||||
const requestDuration = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
duration: requestDuration
|
||||
duration: responseTime ?? 0
|
||||
};
|
||||
} catch (error) {
|
||||
// todo: better error handling
|
||||
// need to convey the error to the UI
|
||||
// and need not be always a network error
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
|
||||
if (axios.isCancel(error)) {
|
||||
let error = new Error('Request cancelled');
|
||||
error.isCancel = true;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (error && error.response) {
|
||||
// run assertions
|
||||
const assertions = get(request, 'assertions');
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const results = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
request,
|
||||
error.response,
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'assertion-results',
|
||||
results: results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
const testFile = compact([
|
||||
get(collectionRoot, 'request.tests'),
|
||||
item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests')
|
||||
]).join(os.EOL);
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime();
|
||||
const testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
error.response,
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results',
|
||||
results: testResults.results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
collectionVariables: testResults.collectionVariables,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// Prevents the duration from leaking to the actual result
|
||||
const requestDuration = error.response.headers.get('request-duration');
|
||||
error.response.headers.delete('request-duration');
|
||||
return {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
duration: requestDuration ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
@@ -519,12 +475,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request, collectionRoot);
|
||||
|
||||
const preferences = getPreferences();
|
||||
const timeout = get(preferences, 'request.timeout', 0);
|
||||
request.timeout = timeout;
|
||||
const sslVerification = get(preferences, 'request.sslVerification', true);
|
||||
request.timeout = preferencesUtil.getRequestTimeout();
|
||||
|
||||
if (!sslVerification) {
|
||||
if (!preferencesUtil.shouldVerifyTls()) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
@@ -533,7 +486,14 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const processEnvVars = getProcessEnvVars(collection.uid);
|
||||
interpolateVars(preparedRequest, envVars, collection.collectionVariables, processEnvVars);
|
||||
|
||||
const response = await axios(preparedRequest);
|
||||
const axiosInstance = await configureRequest(
|
||||
collection.uid,
|
||||
preparedRequest,
|
||||
envVars,
|
||||
collection.collectionVariables,
|
||||
processEnvVars
|
||||
);
|
||||
const response = await axiosInstance(preparedRequest);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
@@ -658,16 +618,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
const preferences = getPreferences();
|
||||
const timeout = get(preferences, 'request.timeout', 0);
|
||||
request.timeout = timeout;
|
||||
const sslVerification = get(preferences, 'request.sslVerification', true);
|
||||
|
||||
// run pre-request script
|
||||
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
|
||||
os.EOL
|
||||
);
|
||||
if (requestScript && requestScript.length) {
|
||||
if (requestScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const result = await scriptRuntime.runRequestScript(
|
||||
decomment(requestScript),
|
||||
@@ -704,65 +659,61 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
...eventData
|
||||
});
|
||||
|
||||
// proxy configuration
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
||||
if (proxyEnabled) {
|
||||
let proxyUri;
|
||||
const interpolationOptions = {
|
||||
envVars,
|
||||
collectionVariables,
|
||||
processEnvVars
|
||||
};
|
||||
const axiosInstance = await configureRequest(
|
||||
collectionUid,
|
||||
request,
|
||||
envVars,
|
||||
collectionVariables,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
|
||||
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
|
||||
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
|
||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(
|
||||
get(brunoConfig, 'proxy.auth.username'),
|
||||
interpolationOptions
|
||||
);
|
||||
|
||||
const proxyAuthPassword = interpolateString(
|
||||
get(brunoConfig, 'proxy.auth.password'),
|
||||
interpolationOptions
|
||||
);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
|
||||
}
|
||||
|
||||
if (socksEnabled) {
|
||||
const socksProxyAgent = new SocksProxyAgent(proxyUri);
|
||||
|
||||
request.httpsAgent = socksProxyAgent;
|
||||
request.httpAgent = socksProxyAgent;
|
||||
} else {
|
||||
request.httpsAgent = new HttpsProxyAgent(proxyUri, {
|
||||
rejectUnauthorized: sslVerification
|
||||
});
|
||||
|
||||
request.httpAgent = new HttpProxyAgent(proxyUri);
|
||||
}
|
||||
} else if (!sslVerification) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
}
|
||||
|
||||
// send request
|
||||
timeStart = Date.now();
|
||||
const response = await axios(request);
|
||||
timeEnd = Date.now();
|
||||
let response;
|
||||
try {
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
timeEnd = Date.now();
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'response-received',
|
||||
responseReceived: {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: Object.entries(response.headers),
|
||||
duration: timeEnd - timeStart,
|
||||
size: response.headers['content-length'] || getSize(response.data),
|
||||
data: response.data
|
||||
},
|
||||
...eventData
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.response) {
|
||||
timeEnd = Date.now();
|
||||
response = {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: Object.entries(error.response.headers),
|
||||
duration: timeEnd - timeStart,
|
||||
size: error.response.headers['content-length'] || getSize(error.response.data),
|
||||
data: error.response.data
|
||||
};
|
||||
|
||||
// if we get a response from the server, we consider it as a success
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'response-received',
|
||||
error: error ? error.message : 'An error occurred while running the request',
|
||||
responseReceived: response,
|
||||
...eventData
|
||||
});
|
||||
} else {
|
||||
// if it's not a network error, don't continue
|
||||
throw Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(request, 'vars.res', []);
|
||||
if (postResponseVars && postResponseVars.length) {
|
||||
if (postResponseVars?.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
const result = varsRuntime.runPostResponseVars(
|
||||
postResponseVars,
|
||||
@@ -861,105 +812,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'response-received',
|
||||
...eventData,
|
||||
responseReceived: {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: Object.entries(response.headers),
|
||||
duration: timeEnd - timeStart,
|
||||
size: response.headers['content-length'] || getSize(response.data),
|
||||
data: response.data
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
let responseReceived = {};
|
||||
let duration = 0;
|
||||
|
||||
if (timeStart && timeEnd) {
|
||||
duration = timeEnd - timeStart;
|
||||
}
|
||||
|
||||
if (error && error.response) {
|
||||
responseReceived = {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: Object.entries(error.response.headers),
|
||||
duration: duration,
|
||||
size: error.response.headers['content-length'] || getSize(error.response.data),
|
||||
data: error.response.data
|
||||
};
|
||||
|
||||
// run assertions
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const results = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
request,
|
||||
error.response,
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'assertion-results',
|
||||
assertionResults: results,
|
||||
itemUid: item.uid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
const testFile = compact([
|
||||
get(collectionRoot, 'request.tests'),
|
||||
item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests')
|
||||
]).join(os.EOL);
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime();
|
||||
const testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
error.response,
|
||||
envVars,
|
||||
collectionVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results',
|
||||
testResults: testResults.results,
|
||||
...eventData
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
collectionVariables: testResults.collectionVariables,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// if we get a response from the server, we consider it as a success
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'response-received',
|
||||
error: error ? error.message : 'An error occurred while running the request',
|
||||
responseReceived: responseReceived,
|
||||
...eventData
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'error',
|
||||
error: error ? error.message : 'An error occurred while running the request',
|
||||
responseReceived: responseReceived,
|
||||
responseReceived: {},
|
||||
...eventData
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const { ipcMain } = require('electron');
|
||||
const { getPreferences, savePreferences } = require('../store/preferences');
|
||||
const { isDirectory } = require('../utils/filesystem');
|
||||
const { openCollection } = require('../app/collections');
|
||||
|
||||
``;
|
||||
const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
ipcMain.handle('renderer:ready', async (event) => {
|
||||
// load preferences
|
||||
@@ -15,7 +15,7 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
if (lastOpened && lastOpened.length) {
|
||||
for (let collectionPath of lastOpened) {
|
||||
if (isDirectory(collectionPath)) {
|
||||
openCollection(mainWindow, watcher, collectionPath, {
|
||||
await openCollection(mainWindow, watcher, collectionPath, {
|
||||
dontSendDisplayErrors: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
const Yup = require('yup');
|
||||
const Store = require('electron-store');
|
||||
const { get } = require('lodash');
|
||||
|
||||
/**
|
||||
* The preferences are stored in the electron store 'preferences.json'.
|
||||
* The electron process uses this module to get the preferences.
|
||||
*
|
||||
*/
|
||||
|
||||
const defaultPreferences = {
|
||||
request: {
|
||||
@@ -8,6 +15,18 @@ const defaultPreferences = {
|
||||
},
|
||||
font: {
|
||||
codeFont: 'default'
|
||||
},
|
||||
proxy: {
|
||||
enabled: false,
|
||||
protocol: 'http',
|
||||
hostname: '',
|
||||
port: '',
|
||||
auth: {
|
||||
enabled: false,
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
bypassProxy: ''
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,6 +37,18 @@ const preferencesSchema = Yup.object().shape({
|
||||
}),
|
||||
font: Yup.object().shape({
|
||||
codeFont: Yup.string().nullable()
|
||||
}),
|
||||
proxy: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||
hostname: Yup.string().max(1024),
|
||||
port: Yup.number().min(1).max(65535).nullable(),
|
||||
auth: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string().max(1024),
|
||||
password: Yup.string().max(1024)
|
||||
}).optional(),
|
||||
bypassProxy: Yup.string().optional().max(1024)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -61,7 +92,20 @@ const savePreferences = async (newPreferences) => {
|
||||
});
|
||||
};
|
||||
|
||||
const preferencesUtil = {
|
||||
shouldVerifyTls: () => {
|
||||
return get(getPreferences(), 'request.sslVerification', true);
|
||||
},
|
||||
getRequestTimeout: () => {
|
||||
return get(getPreferences(), 'request.timeout', 0);
|
||||
},
|
||||
getGlobalProxyConfig: () => {
|
||||
return get(getPreferences(), 'proxy', {});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getPreferences,
|
||||
savePreferences
|
||||
savePreferences,
|
||||
preferencesUtil
|
||||
};
|
||||
|
||||
66
packages/bruno-electron/src/utils/proxy-util.js
Normal file
66
packages/bruno-electron/src/utils/proxy-util.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const parseUrl = require('url').parse;
|
||||
const { isEmpty } = require('lodash');
|
||||
|
||||
const DEFAULT_PORTS = {
|
||||
ftp: 21,
|
||||
gopher: 70,
|
||||
http: 80,
|
||||
https: 443,
|
||||
ws: 80,
|
||||
wss: 443
|
||||
};
|
||||
/**
|
||||
* check for proxy bypass, copied form 'proxy-from-env'
|
||||
*/
|
||||
const shouldUseProxy = (url, proxyBypass) => {
|
||||
if (proxyBypass === '*') {
|
||||
return false; // Never proxy if wildcard is set.
|
||||
}
|
||||
|
||||
// use proxy if no proxyBypass is set
|
||||
if (!proxyBypass || typeof proxyBypass !== 'string' || isEmpty(proxyBypass.trim())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {};
|
||||
let proto = parsedUrl.protocol;
|
||||
let hostname = parsedUrl.host;
|
||||
let port = parsedUrl.port;
|
||||
if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') {
|
||||
return false; // Don't proxy URLs without a valid scheme or host.
|
||||
}
|
||||
|
||||
proto = proto.split(':', 1)[0];
|
||||
// Stripping ports in this way instead of using parsedUrl.hostname to make
|
||||
// sure that the brackets around IPv6 addresses are kept.
|
||||
hostname = hostname.replace(/:\d*$/, '');
|
||||
port = parseInt(port) || DEFAULT_PORTS[proto] || 0;
|
||||
|
||||
return proxyBypass.split(/[,;\s]/).every(function (dontProxyFor) {
|
||||
if (!dontProxyFor) {
|
||||
return true; // Skip zero-length hosts.
|
||||
}
|
||||
const parsedProxy = dontProxyFor.match(/^(.+):(\d+)$/);
|
||||
let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor;
|
||||
const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0;
|
||||
if (parsedProxyPort && parsedProxyPort !== port) {
|
||||
return true; // Skip if ports don't match.
|
||||
}
|
||||
|
||||
if (!/^[.*]/.test(parsedProxyHostname)) {
|
||||
// No wildcards, so stop proxying if there is an exact match.
|
||||
return hostname !== parsedProxyHostname;
|
||||
}
|
||||
|
||||
if (parsedProxyHostname.charAt(0) === '*') {
|
||||
// Remove leading wildcard.
|
||||
parsedProxyHostname = parsedProxyHostname.slice(1);
|
||||
}
|
||||
// Stop proxying if the hostname ends with the no_proxy host.
|
||||
return !hostname.endsWith(parsedProxyHostname);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
shouldUseProxy
|
||||
};
|
||||
50
packages/bruno-electron/tests/utils/proxy-util.spec.js
Normal file
50
packages/bruno-electron/tests/utils/proxy-util.spec.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const { shouldUseProxy } = require('../../src/utils/proxy-util');
|
||||
|
||||
test('no proxy necessary - star', () => {
|
||||
const url = 'http://wwww.example.org/test';
|
||||
const noProxy = '*';
|
||||
|
||||
expect(shouldUseProxy(url, noProxy)).toEqual(false);
|
||||
});
|
||||
|
||||
test('no proxy necessary - no noProxy bypass', () => {
|
||||
const url = 'http://wwww.example.org/test';
|
||||
const noProxy = '';
|
||||
|
||||
expect(shouldUseProxy(url, noProxy)).toEqual(true);
|
||||
});
|
||||
|
||||
test('no proxy necessary - wildcard match', () => {
|
||||
const url = 'http://wwww.example.org/test';
|
||||
const noProxy = '*example.org';
|
||||
|
||||
expect(shouldUseProxy(url, noProxy)).toEqual(false);
|
||||
});
|
||||
|
||||
test('no proxy necessary - direct proxy', () => {
|
||||
const url = 'http://wwww.example.org/test';
|
||||
const noProxy = 'wwww.example.org';
|
||||
|
||||
expect(shouldUseProxy(url, noProxy)).toEqual(false);
|
||||
});
|
||||
|
||||
test('no proxy necessary - multiple proxy', () => {
|
||||
const url = 'http://wwww.example.org/test';
|
||||
const noProxy = 'www.example.com,wwww.example.org';
|
||||
|
||||
expect(shouldUseProxy(url, noProxy)).toEqual(false);
|
||||
});
|
||||
|
||||
test('proxy necessary - no proxy match multiple', () => {
|
||||
const url = 'https://wwww.example.test/test';
|
||||
const noProxy = 'www.example.com,wwww.example.org';
|
||||
|
||||
expect(shouldUseProxy(url, noProxy)).toEqual(true);
|
||||
});
|
||||
|
||||
test('proxy necessary - no proxy match', () => {
|
||||
const url = 'https://wwww.example.test/test';
|
||||
const noProxy = 'www.example.com';
|
||||
|
||||
expect(shouldUseProxy(url, noProxy)).toEqual(true);
|
||||
});
|
||||
55
packages/bruno-js/src/interpolate-string.js
Normal file
55
packages/bruno-js/src/interpolate-string.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const Handlebars = require('handlebars');
|
||||
const { forOwn, cloneDeep } = require('lodash');
|
||||
|
||||
const interpolateEnvVars = (str, processEnvVars) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
const template = Handlebars.compile(str, { noEscape: true });
|
||||
|
||||
return template({
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const interpolateString = (str, { envVariables, collectionVariables, processEnvVars }) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
processEnvVars = processEnvVars || {};
|
||||
collectionVariables = collectionVariables || {};
|
||||
|
||||
// we clone envVariables because we don't want to modify the original object
|
||||
envVariables = envVariables ? cloneDeep(envVariables) : {};
|
||||
|
||||
// envVariables can inturn have values as {{process.env.VAR_NAME}}
|
||||
// so we need to interpolate envVariables first with processEnvVars
|
||||
forOwn(envVariables, (value, key) => {
|
||||
envVariables[key] = interpolateEnvVars(value, processEnvVars);
|
||||
});
|
||||
|
||||
const template = Handlebars.compile(str, { noEscape: true });
|
||||
|
||||
// collectionVariables take precedence over envVariables
|
||||
const combinedVars = {
|
||||
...envVariables,
|
||||
...collectionVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return template(combinedVars);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
interpolateString
|
||||
};
|
||||
@@ -4,6 +4,7 @@ const { nanoid } = require('nanoid');
|
||||
const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
|
||||
const { interpolateString } = require('../interpolate-string');
|
||||
|
||||
const { expect } = chai;
|
||||
chai.use(require('chai-string'));
|
||||
@@ -161,17 +162,27 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const interpolationContext = {
|
||||
collectionVariables: context.bru.collectionVariables,
|
||||
envVariables: context.bru.envVariables,
|
||||
processEnvVars: context.bru.processEnvVars
|
||||
};
|
||||
|
||||
// gracefully allow both a,b as well as [a, b]
|
||||
if (operator === 'in' || operator === 'notIn') {
|
||||
if (rhsOperand.startsWith('[') && rhsOperand.endsWith(']')) {
|
||||
rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);
|
||||
}
|
||||
|
||||
return rhsOperand.split(',').map((v) => evaluateJsTemplateLiteral(v.trim(), context));
|
||||
return rhsOperand
|
||||
.split(',')
|
||||
.map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
|
||||
}
|
||||
|
||||
if (operator === 'between') {
|
||||
const [lhs, rhs] = rhsOperand.split(',').map((v) => evaluateJsTemplateLiteral(v.trim(), context));
|
||||
const [lhs, rhs] = rhsOperand
|
||||
.split(',')
|
||||
.map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
|
||||
return [lhs, rhs];
|
||||
}
|
||||
|
||||
@@ -181,10 +192,10 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
|
||||
rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);
|
||||
}
|
||||
|
||||
return rhsOperand;
|
||||
return interpolateString(rhsOperand, interpolationContext);
|
||||
}
|
||||
|
||||
return evaluateJsTemplateLiteral(rhsOperand, context);
|
||||
return evaluateJsTemplateLiteral(interpolateString(rhsOperand, interpolationContext), context);
|
||||
};
|
||||
|
||||
class AssertRuntime {
|
||||
|
||||
Reference in New Issue
Block a user