mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-29 03:13:45 +01:00
Merge pull request #596 from mirkogolze/feature/proxy-global-and-collection
proxy settings on global and collection level
This commit is contained in:
commit
d767a144f2
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,7 +1,9 @@
|
|||||||
# Description
|
# Description
|
||||||
|
|
||||||
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
|
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
|
||||||
|
|
||||||
# Contribution Checklist:
|
# Contribution Checklist:
|
||||||
|
|
||||||
- [ ] **The pull request does not introduce any breaking changes**
|
- [ ] **The pull request does not introduce any breaking changes**
|
||||||
- [ ] **I have read the [contribution guidelines](https://github.com/usebruno/bruno/blob/main/contributing.md).**
|
- [ ] **I have read the [contribution guidelines](https://github.com/usebruno/bruno/blob/main/contributing.md).**
|
||||||
- [ ] **Create an issue and link to the pull request.**
|
- [ ] **Create an issue and link to the pull request.**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'github-markdown-css/github-markdown.css';
|
import 'github-markdown-css/github-markdown.css';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
|
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
|
||||||
import { useTheme } from 'providers/Theme/index';
|
import { useTheme } from 'providers/Theme';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
@ -1,13 +1,55 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||||
|
const proxySchema = Yup.object({
|
||||||
|
enabled: Yup.string().oneOf(['global', 'enabled', 'disabled']),
|
||||||
|
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||||
|
hostname: Yup.string()
|
||||||
|
.when('enabled', {
|
||||||
|
is: 'enabled',
|
||||||
|
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||||
|
otherwise: (hostname) => hostname.nullable()
|
||||||
|
})
|
||||||
|
.max(1024),
|
||||||
|
port: Yup.number()
|
||||||
|
.when('enabled', {
|
||||||
|
is: 'enabled',
|
||||||
|
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: 'enabled',
|
||||||
|
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(),
|
||||||
|
noProxy: Yup.string().optional().max(1024)
|
||||||
|
});
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
enabled: proxyConfig.enabled || false,
|
enabled: proxyConfig.enabled || 'global',
|
||||||
protocol: proxyConfig.protocol || 'http',
|
protocol: proxyConfig.protocol || 'http',
|
||||||
hostname: proxyConfig.hostname || '',
|
hostname: proxyConfig.hostname || '',
|
||||||
port: proxyConfig.port || '',
|
port: proxyConfig.port || '',
|
||||||
@ -15,27 +57,26 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||||
}
|
},
|
||||||
|
noProxy: proxyConfig.noProxy || ''
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: proxySchema,
|
||||||
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)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
onUpdate(values);
|
proxySchema
|
||||||
|
.validate(values, { abortEarly: true })
|
||||||
|
.then((validatedProxy) => {
|
||||||
|
onUpdate(validatedProxy);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
let errMsg = error.message || 'Preferences validation error';
|
||||||
|
toast.error(errMsg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.setValues({
|
formik.setValues({
|
||||||
enabled: proxyConfig.enabled || false,
|
enabled: proxyConfig.enabled || 'global',
|
||||||
protocol: proxyConfig.protocol || 'http',
|
protocol: proxyConfig.protocol || 'http',
|
||||||
hostname: proxyConfig.hostname || '',
|
hostname: proxyConfig.hostname || '',
|
||||||
port: proxyConfig.port || '',
|
port: proxyConfig.port || '',
|
||||||
@ -43,18 +84,61 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||||
}
|
},
|
||||||
|
noProxy: proxyConfig.noProxy || ''
|
||||||
});
|
});
|
||||||
}, [proxyConfig]);
|
}, [proxyConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
|
<h1 className="font-medium mb-3">Proxy Settings</h1>
|
||||||
|
<label className="settings-label">
|
||||||
|
<ul className="mb-3">
|
||||||
|
<li>global - use global config</li>
|
||||||
|
<li>enabled - use collection config</li>
|
||||||
|
<li>disable - disable proxy</li>
|
||||||
|
</ul>
|
||||||
|
</label>
|
||||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||||
<div className="mb-3 flex items-center">
|
<div className="mb-3 flex items-center">
|
||||||
<label className="settings-label" htmlFor="enabled">
|
<label className="settings-label" htmlFor="enabled">
|
||||||
Enabled
|
Config
|
||||||
</label>
|
</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="enabled"
|
||||||
|
value="global"
|
||||||
|
checked={formik.values.enabled === 'global'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
global
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center ml-4">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="enabled"
|
||||||
|
value="enabled"
|
||||||
|
checked={formik.values.enabled === 'enabled'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
enabled
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center ml-4">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="enabled"
|
||||||
|
value="disabled"
|
||||||
|
checked={formik.values.enabled === 'disabled'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
disabled
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 flex items-center">
|
<div className="mb-3 flex items-center">
|
||||||
<label className="settings-label" htmlFor="protocol">
|
<label className="settings-label" htmlFor="protocol">
|
||||||
@ -83,6 +167,17 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
/>
|
/>
|
||||||
https
|
https
|
||||||
</label>
|
</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">
|
<label className="flex items-center ml-4">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -113,7 +208,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
value={formik.values.hostname || ''}
|
value={formik.values.hostname || ''}
|
||||||
/>
|
/>
|
||||||
{formik.touched.hostname && formik.errors.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}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 flex items-center">
|
<div className="mb-3 flex items-center">
|
||||||
@ -132,7 +227,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
value={formik.values.port}
|
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>
|
||||||
<div className="mb-3 flex items-center">
|
<div className="mb-3 flex items-center">
|
||||||
<label className="settings-label" htmlFor="auth.enabled">
|
<label className="settings-label" htmlFor="auth.enabled">
|
||||||
@ -163,7 +260,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
{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}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 flex items-center">
|
<div className="mb-3 flex items-center">
|
||||||
@ -183,10 +280,30 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
{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}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="noProxy">
|
||||||
|
Proxy Bypass
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="noProxy"
|
||||||
|
type="text"
|
||||||
|
name="noProxy"
|
||||||
|
className="block textbox"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.noProxy || ''}
|
||||||
|
/>
|
||||||
|
{formik.touched.noProxy && formik.errors.noProxy ? (
|
||||||
|
<div className="ml-3 text-red-500">{formik.errors.noProxy}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||||
Save
|
Save
|
||||||
|
@ -36,7 +36,7 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
brunoConfig.proxy = config;
|
brunoConfig.proxy = config;
|
||||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||||
.then(() => {
|
.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'));
|
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||||
};
|
};
|
||||||
|
@ -33,7 +33,7 @@ const General = ({ close }) => {
|
|||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<div className="flex items-center mt-2">
|
<div className="flex items-center mt-2">
|
||||||
<label className="mr-2 select-none" style={{ minWidth: 200 }} htmlFor="ssl-cert-verification">
|
<label className="mr-2 select-none" style={{ minWidth: 200 }} htmlFor="ssl-cert-verification">
|
||||||
SSL Certificate Verification
|
TLS Certificate Verification
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="ssl-cert-verification"
|
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(),
|
||||||
|
noProxy: 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 || '' : ''
|
||||||
|
},
|
||||||
|
noProxy: preferences.proxy.noProxy || ''
|
||||||
|
},
|
||||||
|
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 || '' : ''
|
||||||
|
},
|
||||||
|
noProxy: preferences.proxy.noProxy || ''
|
||||||
|
});
|
||||||
|
}, [preferences]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper>
|
||||||
|
<h1 className="font-medium mb-3">Proxy Settings</h1>
|
||||||
|
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||||
|
<div className="ml-4 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="ml-4 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="ml-4 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="ml-4 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="ml-4 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="ml-4 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="ml-4 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="ml-4 mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="noProxy">
|
||||||
|
Proxy Bypass
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="noProxy"
|
||||||
|
type="text"
|
||||||
|
name="noProxy"
|
||||||
|
className="block textbox"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.noProxy || ''}
|
||||||
|
/>
|
||||||
|
{formik.touched.noProxy && formik.errors.noProxy ? (
|
||||||
|
<div className="ml-3 text-red-500">{formik.errors.noProxy}</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 General from './General';
|
||||||
import Font from './Font';
|
import Font from './Font';
|
||||||
import Theme from './Theme';
|
import Theme from './Theme';
|
||||||
|
import Proxy from './ProxySettings';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const Preferences = ({ onClose }) => {
|
const Preferences = ({ onClose }) => {
|
||||||
@ -22,6 +23,10 @@ const Preferences = ({ onClose }) => {
|
|||||||
return <General close={onClose} />;
|
return <General close={onClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'proxy': {
|
||||||
|
return <Proxy close={onClose} />;
|
||||||
|
}
|
||||||
|
|
||||||
case 'theme': {
|
case 'theme': {
|
||||||
return <Theme close={onClose} />;
|
return <Theme close={onClose} />;
|
||||||
}
|
}
|
||||||
@ -49,6 +54,9 @@ const Preferences = ({ onClose }) => {
|
|||||||
<div className={getTabClassname('font')} role="tab" onClick={() => setTab('font')}>
|
<div className={getTabClassname('font')} role="tab" onClick={() => setTab('font')}>
|
||||||
Font
|
Font
|
||||||
</div>
|
</div>
|
||||||
|
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||||
|
Proxy
|
||||||
|
</div>
|
||||||
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
||||||
Support
|
Support
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,6 +45,8 @@ export const fetchGqlSchema = async (endpoint, environment, request, collection)
|
|||||||
|
|
||||||
export const cancelNetworkRequest = async (cancelTokenUid) => {
|
export const cancelNetworkRequest = async (cancelTokenUid) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
ipcRenderer.invoke('cancel-http-request', cancelTokenUid).then(resolve).catch(reject);
|
ipcRenderer.invoke('cancel-http-request', cancelTokenUid).then(resolve).catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -162,9 +162,7 @@ const getCollectionRoot = (dir) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(collectionRootPath, 'utf8');
|
const content = fs.readFileSync(collectionRootPath, 'utf8');
|
||||||
const json = collectionBruToJson(content);
|
return collectionBruToJson(content);
|
||||||
|
|
||||||
return json;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const builder = async (yargs) => {
|
const builder = async (yargs) => {
|
||||||
|
@ -16,6 +16,7 @@ const { HttpsProxyAgent } = require('https-proxy-agent');
|
|||||||
const { HttpProxyAgent } = require('http-proxy-agent');
|
const { HttpProxyAgent } = require('http-proxy-agent');
|
||||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||||
const { makeAxiosInstance } = require('../utils/axios-instance');
|
const { makeAxiosInstance } = require('../utils/axios-instance');
|
||||||
|
const { shouldUseProxy } = require('../utils/proxy-util');
|
||||||
|
|
||||||
const runSingleRequest = async function (
|
const runSingleRequest = async function (
|
||||||
filename,
|
filename,
|
||||||
@ -47,7 +48,7 @@ const runSingleRequest = async function (
|
|||||||
|
|
||||||
// run pre-request vars
|
// run pre-request vars
|
||||||
const preRequestVars = get(bruJson, 'request.vars.req');
|
const preRequestVars = get(bruJson, 'request.vars.req');
|
||||||
if (preRequestVars && preRequestVars.length) {
|
if (preRequestVars?.length) {
|
||||||
const varsRuntime = new VarsRuntime();
|
const varsRuntime = new VarsRuntime();
|
||||||
varsRuntime.runPreRequestVars(
|
varsRuntime.runPreRequestVars(
|
||||||
preRequestVars,
|
preRequestVars,
|
||||||
@ -64,7 +65,7 @@ const runSingleRequest = async function (
|
|||||||
get(collectionRoot, 'request.script.req'),
|
get(collectionRoot, 'request.script.req'),
|
||||||
get(bruJson, 'request.script.req')
|
get(bruJson, 'request.script.req')
|
||||||
]).join(os.EOL);
|
]).join(os.EOL);
|
||||||
if (requestScriptFile && requestScriptFile.length) {
|
if (requestScriptFile?.length) {
|
||||||
const scriptRuntime = new ScriptRuntime();
|
const scriptRuntime = new ScriptRuntime();
|
||||||
await scriptRuntime.runRequestScript(
|
await scriptRuntime.runRequestScript(
|
||||||
decomment(requestScriptFile),
|
decomment(requestScriptFile),
|
||||||
@ -87,36 +88,56 @@ const runSingleRequest = async function (
|
|||||||
if (insecure) {
|
if (insecure) {
|
||||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
||||||
} else {
|
} else {
|
||||||
const cacertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
|
const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
|
||||||
const cacert = cacertArray.find((el) => el);
|
const caCert = caCertArray.find((el) => el);
|
||||||
if (cacert && cacert.length > 1) {
|
if (caCert && caCert.length > 1) {
|
||||||
try {
|
try {
|
||||||
caCrt = fs.readFileSync(cacert);
|
httpsAgentRequestFields['ca'] = fs.readFileSync(caCert);
|
||||||
httpsAgentRequestFields['ca'] = caCrt;
|
|
||||||
} catch (err) {
|
} 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
|
// set proxy if enabled
|
||||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
||||||
if (proxyEnabled) {
|
const shouldProxy = shouldUseProxy(request.url, get(brunoConfig, 'proxy.noProxy', ''));
|
||||||
let proxyUri;
|
if (proxyEnabled && shouldProxy) {
|
||||||
const interpolationOptions = {
|
|
||||||
envVars: envVariables,
|
|
||||||
collectionVariables,
|
|
||||||
processEnvVars
|
|
||||||
};
|
|
||||||
|
|
||||||
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
|
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
|
||||||
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
|
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
|
||||||
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
|
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
|
||||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||||
const socksEnabled = proxyProtocol.includes('socks');
|
const socksEnabled = proxyProtocol.includes('socks');
|
||||||
|
|
||||||
interpolateString;
|
let proxyUri;
|
||||||
|
|
||||||
if (proxyAuthEnabled) {
|
if (proxyAuthEnabled) {
|
||||||
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
|
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
|
||||||
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
|
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
|
||||||
@ -128,16 +149,13 @@ const runSingleRequest = async function (
|
|||||||
|
|
||||||
if (socksEnabled) {
|
if (socksEnabled) {
|
||||||
const socksProxyAgent = new SocksProxyAgent(proxyUri);
|
const socksProxyAgent = new SocksProxyAgent(proxyUri);
|
||||||
|
|
||||||
request.httpsAgent = socksProxyAgent;
|
request.httpsAgent = socksProxyAgent;
|
||||||
|
|
||||||
request.httpAgent = socksProxyAgent;
|
request.httpAgent = socksProxyAgent;
|
||||||
} else {
|
} else {
|
||||||
request.httpsAgent = new HttpsProxyAgent(
|
request.httpsAgent = new HttpsProxyAgent(
|
||||||
proxyUri,
|
proxyUri,
|
||||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
request.httpAgent = new HttpProxyAgent(proxyUri);
|
request.httpAgent = new HttpProxyAgent(proxyUri);
|
||||||
}
|
}
|
||||||
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
||||||
@ -163,7 +181,7 @@ const runSingleRequest = async function (
|
|||||||
responseTime = response.headers.get('request-duration');
|
responseTime = response.headers.get('request-duration');
|
||||||
response.headers.delete('request-duration');
|
response.headers.delete('request-duration');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err && err.response) {
|
if (err?.response) {
|
||||||
response = err.response;
|
response = err.response;
|
||||||
|
|
||||||
// Prevents the duration on leaking to the actual result
|
// Prevents the duration on leaking to the actual result
|
||||||
@ -199,7 +217,7 @@ const runSingleRequest = async function (
|
|||||||
|
|
||||||
// run post-response vars
|
// run post-response vars
|
||||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||||
if (postResponseVars && postResponseVars.length) {
|
if (postResponseVars?.length) {
|
||||||
const varsRuntime = new VarsRuntime();
|
const varsRuntime = new VarsRuntime();
|
||||||
varsRuntime.runPostResponseVars(
|
varsRuntime.runPostResponseVars(
|
||||||
postResponseVars,
|
postResponseVars,
|
||||||
@ -217,7 +235,7 @@ const runSingleRequest = async function (
|
|||||||
get(collectionRoot, 'request.script.res'),
|
get(collectionRoot, 'request.script.res'),
|
||||||
get(bruJson, 'request.script.res')
|
get(bruJson, 'request.script.res')
|
||||||
]).join(os.EOL);
|
]).join(os.EOL);
|
||||||
if (responseScriptFile && responseScriptFile.length) {
|
if (responseScriptFile?.length) {
|
||||||
const scriptRuntime = new ScriptRuntime();
|
const scriptRuntime = new ScriptRuntime();
|
||||||
await scriptRuntime.runResponseScript(
|
await scriptRuntime.runResponseScript(
|
||||||
decomment(responseScriptFile),
|
decomment(responseScriptFile),
|
||||||
@ -275,7 +293,7 @@ const runSingleRequest = async function (
|
|||||||
testResults = get(result, 'results', []);
|
testResults = get(result, 'results', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (testResults && testResults.length) {
|
if (testResults?.length) {
|
||||||
each(testResults, (testResult) => {
|
each(testResults, (testResult) => {
|
||||||
if (testResult.status === 'pass') {
|
if (testResult.status === 'pass') {
|
||||||
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
||||||
|
@ -4,10 +4,10 @@ const axios = require('axios');
|
|||||||
* Function that configures axios with timing interceptors
|
* Function that configures axios with timing interceptors
|
||||||
* Important to note here that the timings are not completely accurate.
|
* Important to note here that the timings are not completely accurate.
|
||||||
* @see https://github.com/axios/axios/issues/695
|
* @see https://github.com/axios/axios/issues/695
|
||||||
* @returns {import('axios').AxiosStatic}
|
* @returns {axios.AxiosInstance}
|
||||||
*/
|
*/
|
||||||
function makeAxiosInstance() {
|
function makeAxiosInstance() {
|
||||||
/** @type {import('axios').AxiosStatic} */
|
/** @type {axios.AxiosInstance} */
|
||||||
const instance = axios.create();
|
const instance = axios.create();
|
||||||
|
|
||||||
instance.interceptors.request.use((config) => {
|
instance.interceptors.request.use((config) => {
|
||||||
@ -26,9 +26,7 @@ function makeAxiosInstance() {
|
|||||||
if (error.response) {
|
if (error.response) {
|
||||||
const end = Date.now();
|
const end = Date.now();
|
||||||
const start = error.config.headers['request-start-time'];
|
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);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
65
packages/bruno-cli/src/utils/proxy-util.js
Normal file
65
packages/bruno-cli/src/utils/proxy-util.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proxyByPass) {
|
||||||
|
return true; // use proxy if enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
@ -75,7 +75,7 @@ app.on('ready', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// register all ipc handlers
|
// register all ipc handlers
|
||||||
registerNetworkIpc(mainWindow, watcher, lastOpenedCollections);
|
registerNetworkIpc(mainWindow);
|
||||||
registerCollectionsIpc(mainWindow, watcher, lastOpenedCollections);
|
registerCollectionsIpc(mainWindow, watcher, lastOpenedCollections);
|
||||||
registerPreferencesIpc(mainWindow, watcher, lastOpenedCollections);
|
registerPreferencesIpc(mainWindow, watcher, lastOpenedCollections);
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@ const { stringifyJson } = require('../utils/common');
|
|||||||
const { openCollectionDialog } = require('../app/collections');
|
const { openCollectionDialog } = require('../app/collections');
|
||||||
const { generateUidBasedOnHash } = require('../utils/common');
|
const { generateUidBasedOnHash } = require('../utils/common');
|
||||||
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
|
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
|
||||||
|
const { setPreferences } = require('../store/preferences');
|
||||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||||
|
|
||||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||||
@ -32,9 +33,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
// browse directory
|
// browse directory
|
||||||
ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => {
|
ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => {
|
||||||
try {
|
try {
|
||||||
const dirPath = await browseDirectory(mainWindow);
|
return await browseDirectory(mainWindow);
|
||||||
|
|
||||||
return dirPath;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
@ -67,8 +66,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
|
|
||||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
||||||
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
|
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
@ -93,8 +90,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
collectionPathname,
|
collectionPathname,
|
||||||
newName
|
newName
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
@ -311,7 +306,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
|
|
||||||
fs.unlinkSync(pathname);
|
fs.unlinkSync(pathname);
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(error);
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
@ -4,10 +4,10 @@ const axios = require('axios');
|
|||||||
* Function that configures axios with timing interceptors
|
* Function that configures axios with timing interceptors
|
||||||
* Important to note here that the timings are not completely accurate.
|
* Important to note here that the timings are not completely accurate.
|
||||||
* @see https://github.com/axios/axios/issues/695
|
* @see https://github.com/axios/axios/issues/695
|
||||||
* @returns {import('axios').AxiosStatic}
|
* @returns {axios.AxiosInstance}
|
||||||
*/
|
*/
|
||||||
function makeAxiosInstance() {
|
function makeAxiosInstance() {
|
||||||
/** @type {import('axios').AxiosStatic} */
|
/** @type {axios.AxiosInstance} */
|
||||||
const instance = axios.create();
|
const instance = axios.create();
|
||||||
|
|
||||||
instance.interceptors.request.use((config) => {
|
instance.interceptors.request.use((config) => {
|
||||||
|
@ -16,7 +16,7 @@ const { uuid } = require('../../utils/common');
|
|||||||
const interpolateVars = require('./interpolate-vars');
|
const interpolateVars = require('./interpolate-vars');
|
||||||
const { interpolateString } = require('./interpolate-string');
|
const { interpolateString } = require('./interpolate-string');
|
||||||
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
|
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
|
||||||
const { getPreferences } = require('../../store/preferences');
|
const { preferences } = require('../../store/preferences');
|
||||||
const { getProcessEnvVars } = require('../../store/process-env');
|
const { getProcessEnvVars } = require('../../store/process-env');
|
||||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||||
@ -24,6 +24,7 @@ const { HttpProxyAgent } = require('http-proxy-agent');
|
|||||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||||
const { makeAxiosInstance } = require('./axios-instance');
|
const { makeAxiosInstance } = require('./axios-instance');
|
||||||
const { addAwsV4Interceptor, resolveCredentials } = require('./awsv4auth-helper');
|
const { addAwsV4Interceptor, resolveCredentials } = require('./awsv4auth-helper');
|
||||||
|
const { shouldUseProxy } = require('../../utils/proxy-util');
|
||||||
|
|
||||||
// override the default escape function to prevent escaping
|
// override the default escape function to prevent escaping
|
||||||
Mustache.escape = function (value) {
|
Mustache.escape = function (value) {
|
||||||
@ -83,6 +84,96 @@ const getSize = (data) => {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const configureRequest = async (collectionUid, request, envVars, collectionVariables, processEnvVars) => {
|
||||||
|
const httpsAgentRequestFields = {};
|
||||||
|
if (!preferences.isTlsVerification()) {
|
||||||
|
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 = preferences.getProxyConfig();
|
||||||
|
proxyEnabled = get(proxyConfig, 'enabled', false);
|
||||||
|
}
|
||||||
|
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'noProxy', ''));
|
||||||
|
if ((proxyEnabled === true || proxyEnabled === 'enabled') && 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 resolveCredentials(request);
|
||||||
|
addAwsV4Interceptor(axiosInstance, request);
|
||||||
|
delete request.awsv4config;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.timeout = preferences.getTimeout();
|
||||||
|
|
||||||
|
return axiosInstance;
|
||||||
|
};
|
||||||
|
|
||||||
const registerNetworkIpc = (mainWindow) => {
|
const registerNetworkIpc = (mainWindow) => {
|
||||||
// handler for sending http request
|
// handler for sending http request
|
||||||
ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
|
ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
|
||||||
@ -134,7 +225,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
// run pre-request vars
|
// run pre-request vars
|
||||||
const preRequestVars = get(request, 'vars.req', []);
|
const preRequestVars = get(request, 'vars.req', []);
|
||||||
if (preRequestVars && preRequestVars.length) {
|
if (preRequestVars?.length) {
|
||||||
const varsRuntime = new VarsRuntime();
|
const varsRuntime = new VarsRuntime();
|
||||||
const result = varsRuntime.runPreRequestVars(
|
const result = varsRuntime.runPreRequestVars(
|
||||||
preRequestVars,
|
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
|
// run pre-request script
|
||||||
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
|
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
|
||||||
os.EOL
|
os.EOL
|
||||||
);
|
);
|
||||||
if (requestScript && requestScript.length) {
|
if (requestScript?.length) {
|
||||||
const scriptRuntime = new ScriptRuntime();
|
const scriptRuntime = new ScriptRuntime();
|
||||||
const result = await scriptRuntime.runRequestScript(
|
const result = await scriptRuntime.runRequestScript(
|
||||||
decomment(requestScript),
|
decomment(requestScript),
|
||||||
@ -209,96 +296,20 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
cancelTokenUid
|
cancelTokenUid
|
||||||
});
|
});
|
||||||
|
|
||||||
const sslVerification = get(preferences, 'request.sslVerification', true);
|
const axiosInstance = await configureRequest(
|
||||||
const httpsAgentRequestFields = {};
|
collectionUid,
|
||||||
if (!sslVerification) {
|
request,
|
||||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const brunoConfig = getBrunoConfig(collectionUid);
|
|
||||||
const interpolationOptions = {
|
|
||||||
envVars,
|
envVars,
|
||||||
collectionVariables,
|
collectionVariables,
|
||||||
processEnvVars
|
processEnvVars
|
||||||
};
|
);
|
||||||
|
|
||||||
// client certificate config
|
|
||||||
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
|
|
||||||
|
|
||||||
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('*', '.*');
|
|
||||||
|
|
||||||
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
|
|
||||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
|
||||||
if (proxyEnabled) {
|
|
||||||
let proxyUri;
|
|
||||||
|
|
||||||
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,
|
|
||||||
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} */
|
/** @type {import('axios').AxiosResponse} */
|
||||||
const response = await axiosInstance(request);
|
const response = await axiosInstance(request);
|
||||||
|
|
||||||
// run post-response vars
|
// run post-response vars
|
||||||
const postResponseVars = get(request, 'vars.res', []);
|
const postResponseVars = get(request, 'vars.res', []);
|
||||||
if (postResponseVars && postResponseVars.length) {
|
if (postResponseVars?.length) {
|
||||||
const varsRuntime = new VarsRuntime();
|
const varsRuntime = new VarsRuntime();
|
||||||
const result = varsRuntime.runPostResponseVars(
|
const result = varsRuntime.runPostResponseVars(
|
||||||
postResponseVars,
|
postResponseVars,
|
||||||
@ -324,7 +335,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
const responseScript = compact([get(collectionRoot, 'request.script.res'), get(request, 'script.res')]).join(
|
const responseScript = compact([get(collectionRoot, 'request.script.res'), get(request, 'script.res')]).join(
|
||||||
os.EOL
|
os.EOL
|
||||||
);
|
);
|
||||||
if (responseScript && responseScript.length) {
|
if (responseScript?.length) {
|
||||||
const scriptRuntime = new ScriptRuntime();
|
const scriptRuntime = new ScriptRuntime();
|
||||||
const result = await scriptRuntime.runResponseScript(
|
const result = await scriptRuntime.runResponseScript(
|
||||||
decomment(responseScript),
|
decomment(responseScript),
|
||||||
@ -427,7 +438,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error && error.response) {
|
if (error?.response) {
|
||||||
// run assertions
|
// run assertions
|
||||||
const assertions = get(request, 'assertions');
|
const assertions = get(request, 'assertions');
|
||||||
if (assertions) {
|
if (assertions) {
|
||||||
@ -519,12 +530,9 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
const collectionRoot = get(collection, 'root', {});
|
const collectionRoot = get(collection, 'root', {});
|
||||||
const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request, collectionRoot);
|
const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request, collectionRoot);
|
||||||
|
|
||||||
const preferences = getPreferences();
|
request.timeout = preferences.getTimeout();
|
||||||
const timeout = get(preferences, 'request.timeout', 0);
|
|
||||||
request.timeout = timeout;
|
|
||||||
const sslVerification = get(preferences, 'request.sslVerification', true);
|
|
||||||
|
|
||||||
if (!sslVerification) {
|
if (!preferences.isTlsVerification()) {
|
||||||
request.httpsAgent = new https.Agent({
|
request.httpsAgent = new https.Agent({
|
||||||
rejectUnauthorized: false
|
rejectUnauthorized: false
|
||||||
});
|
});
|
||||||
@ -658,20 +666,11 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferences = getPreferences();
|
|
||||||
const timeout = get(preferences, 'request.timeout', 0);
|
|
||||||
request.timeout = timeout;
|
|
||||||
const sslVerification = get(preferences, 'request.sslVerification', true);
|
|
||||||
const httpsAgentRequestFields = {};
|
|
||||||
if (!sslVerification) {
|
|
||||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// run pre-request script
|
// run pre-request script
|
||||||
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
|
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
|
||||||
os.EOL
|
os.EOL
|
||||||
);
|
);
|
||||||
if (requestScript && requestScript.length) {
|
if (requestScript?.length) {
|
||||||
const scriptRuntime = new ScriptRuntime();
|
const scriptRuntime = new ScriptRuntime();
|
||||||
const result = await scriptRuntime.runRequestScript(
|
const result = await scriptRuntime.runRequestScript(
|
||||||
decomment(requestScript),
|
decomment(requestScript),
|
||||||
@ -708,92 +707,22 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
...eventData
|
...eventData
|
||||||
});
|
});
|
||||||
|
|
||||||
const interpolationOptions = {
|
const axiosInstance = await configureRequest(
|
||||||
|
collectionUid,
|
||||||
|
request,
|
||||||
envVars,
|
envVars,
|
||||||
collectionVariables,
|
collectionVariables,
|
||||||
processEnvVars
|
processEnvVars
|
||||||
};
|
);
|
||||||
const brunoConfig = getBrunoConfig(collectionUid);
|
|
||||||
|
|
||||||
// client certificate config
|
|
||||||
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
|
|
||||||
|
|
||||||
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('*', '.*');
|
|
||||||
|
|
||||||
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
|
|
||||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
|
||||||
if (proxyEnabled) {
|
|
||||||
let proxyUri;
|
|
||||||
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,
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// send request
|
|
||||||
timeStart = Date.now();
|
timeStart = Date.now();
|
||||||
const response = await axios(request);
|
/** @type {import('axios').AxiosResponse} */
|
||||||
|
const response = await axiosInstance(request);
|
||||||
timeEnd = Date.now();
|
timeEnd = Date.now();
|
||||||
|
|
||||||
// run post-response vars
|
// run post-response vars
|
||||||
const postResponseVars = get(request, 'vars.res', []);
|
const postResponseVars = get(request, 'vars.res', []);
|
||||||
if (postResponseVars && postResponseVars.length) {
|
if (postResponseVars?.length) {
|
||||||
const varsRuntime = new VarsRuntime();
|
const varsRuntime = new VarsRuntime();
|
||||||
const result = varsRuntime.runPostResponseVars(
|
const result = varsRuntime.runPostResponseVars(
|
||||||
postResponseVars,
|
postResponseVars,
|
||||||
@ -913,7 +842,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
duration = timeEnd - timeStart;
|
duration = timeEnd - timeStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error && error.response) {
|
if (error?.response) {
|
||||||
responseReceived = {
|
responseReceived = {
|
||||||
status: error.response.status,
|
status: error.response.status,
|
||||||
statusText: error.response.statusText,
|
statusText: error.response.statusText,
|
||||||
|
@ -1,9 +1,64 @@
|
|||||||
const { ipcMain } = require('electron');
|
const { ipcMain } = require('electron');
|
||||||
const { getPreferences, savePreferences } = require('../store/preferences');
|
const { getPreferences, savePreferences, getPath } = require('../store/preferences');
|
||||||
const { isDirectory } = require('../utils/filesystem');
|
const { isDirectory } = require('../utils/filesystem');
|
||||||
const { openCollection } = require('../app/collections');
|
const { openCollection } = require('../app/collections');
|
||||||
|
const stores = require('../store');
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||||
|
const change = async (pathname, store) => {
|
||||||
|
if (store === stores.PREFERENCES) {
|
||||||
|
mainWindow.webContents.send('main:load-preferences', getPreferences());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class StoreWatcher {
|
||||||
|
constructor() {
|
||||||
|
this.watchers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
addWatcher(watchPath, store) {
|
||||||
|
console.log(`watcher add: ${watchPath} for store ${store}`);
|
||||||
|
|
||||||
|
if (this.watchers[watchPath]) {
|
||||||
|
this.watchers[watchPath].close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
setTimeout(() => {
|
||||||
|
const watcher = chokidar.watch(watchPath, {
|
||||||
|
ignoreInitial: false,
|
||||||
|
usePolling: false,
|
||||||
|
persistent: true,
|
||||||
|
ignorePermissionErrors: true,
|
||||||
|
awaitWriteFinish: {
|
||||||
|
stabilityThreshold: 80,
|
||||||
|
pollInterval: 10
|
||||||
|
},
|
||||||
|
depth: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('change', (pathname) => change(pathname, store));
|
||||||
|
|
||||||
|
self.watchers[watchPath] = watcher;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWatcher(watchPath) {
|
||||||
|
return this.watchers[watchPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
removeWatcher(watchPath) {
|
||||||
|
if (this.watchers[watchPath]) {
|
||||||
|
this.watchers[watchPath].close();
|
||||||
|
this.watchers[watchPath] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeWatcher = new StoreWatcher();
|
||||||
|
storeWatcher.addWatcher(getPath(), stores.PREFERENCES);
|
||||||
|
|
||||||
ipcMain.handle('renderer:ready', async (event) => {
|
ipcMain.handle('renderer:ready', async (event) => {
|
||||||
// load preferences
|
// load preferences
|
||||||
const preferences = getPreferences();
|
const preferences = getPreferences();
|
||||||
@ -15,7 +70,7 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
|||||||
if (lastOpened && lastOpened.length) {
|
if (lastOpened && lastOpened.length) {
|
||||||
for (let collectionPath of lastOpened) {
|
for (let collectionPath of lastOpened) {
|
||||||
if (isDirectory(collectionPath)) {
|
if (isDirectory(collectionPath)) {
|
||||||
openCollection(mainWindow, watcher, collectionPath, {
|
await openCollection(mainWindow, watcher, collectionPath, {
|
||||||
dontSendDisplayErrors: true
|
dontSendDisplayErrors: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
7
packages/bruno-electron/src/store/index.js
Normal file
7
packages/bruno-electron/src/store/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const PREFERENCES = 'PREFERENCES';
|
||||||
|
|
||||||
|
const stores = {
|
||||||
|
PREFERENCES
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = stores;
|
@ -1,5 +1,12 @@
|
|||||||
const Yup = require('yup');
|
const Yup = require('yup');
|
||||||
const Store = require('electron-store');
|
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 = {
|
const defaultPreferences = {
|
||||||
request: {
|
request: {
|
||||||
@ -8,6 +15,18 @@ const defaultPreferences = {
|
|||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
codeFont: 'default'
|
codeFont: 'default'
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
enabled: false,
|
||||||
|
protocol: 'http',
|
||||||
|
hostnameHttp: '',
|
||||||
|
portHttp: '',
|
||||||
|
auth: {
|
||||||
|
enabled: false,
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
noProxy: ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,6 +37,18 @@ const preferencesSchema = Yup.object().shape({
|
|||||||
}),
|
}),
|
||||||
font: Yup.object().shape({
|
font: Yup.object().shape({
|
||||||
codeFont: Yup.string().nullable()
|
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),
|
||||||
|
auth: Yup.object({
|
||||||
|
enabled: Yup.boolean(),
|
||||||
|
username: Yup.string().max(1024),
|
||||||
|
password: Yup.string().max(1024)
|
||||||
|
}).optional(),
|
||||||
|
noProxy: Yup.string().optional().max(1024)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -29,6 +60,10 @@ class PreferencesStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPath() {
|
||||||
|
return this.store.path;
|
||||||
|
}
|
||||||
|
|
||||||
getPreferences() {
|
getPreferences() {
|
||||||
return {
|
return {
|
||||||
...defaultPreferences,
|
...defaultPreferences,
|
||||||
@ -61,7 +96,27 @@ const savePreferences = async (newPreferences) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPath = () => {
|
||||||
|
return preferencesStore.getPath();
|
||||||
|
};
|
||||||
|
|
||||||
|
const preferences = {
|
||||||
|
isTlsVerification: () => {
|
||||||
|
return get(getPreferences(), 'request.sslVerification', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
getTimeout: () => {
|
||||||
|
return get(getPreferences(), 'request.timeout', 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
getProxyConfig: () => {
|
||||||
|
return get(getPreferences(), 'proxy', {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getPreferences,
|
getPreferences,
|
||||||
savePreferences
|
savePreferences,
|
||||||
|
getPath,
|
||||||
|
preferences
|
||||||
};
|
};
|
||||||
|
64
packages/bruno-electron/src/utils/proxy-util.js
Normal file
64
packages/bruno-electron/src/utils/proxy-util.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proxyByPass) {
|
||||||
|
return true; // use proxy if enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
@ -1,6 +1,6 @@
|
|||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { HomePage } = require('../tests/pages/home.page');
|
const { HomePage } = require('../tests/pages/home.page');
|
||||||
import * as faker from './utils/data-faker';
|
const { faker } = require('./utils/data-faker');
|
||||||
|
|
||||||
test.describe('bruno e2e test', () => {
|
test.describe('bruno e2e test', () => {
|
||||||
let homePage;
|
let homePage;
|
||||||
|
Loading…
Reference in New Issue
Block a user