mirror of
https://github.com/usebruno/bruno.git
synced 2025-06-24 14:01:29 +02:00
Merge branch 'main' into fix/save-prompt-close-multi-tab
This commit is contained in:
commit
e5c718f9f7
@ -55,6 +55,7 @@ if (!SERVER_RENDERED) {
|
|||||||
'req.setMaxRedirects(maxRedirects)',
|
'req.setMaxRedirects(maxRedirects)',
|
||||||
'req.getTimeout()',
|
'req.getTimeout()',
|
||||||
'req.setTimeout(timeout)',
|
'req.setTimeout(timeout)',
|
||||||
|
'req.getExecutionMode()',
|
||||||
'bru',
|
'bru',
|
||||||
'bru.cwd()',
|
'bru.cwd()',
|
||||||
'bru.getEnvName(key)',
|
'bru.getEnvName(key)',
|
||||||
|
@ -52,6 +52,15 @@ const AuthMode = ({ collection }) => {
|
|||||||
>
|
>
|
||||||
Basic Auth
|
Basic Auth
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => {
|
||||||
|
dropdownTippyRef.current.hide();
|
||||||
|
onModeChange('wsse');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
WSSE Auth
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
label {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-line-editor-wrapper {
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: solid 1px ${(props) => props.theme.input.border};
|
||||||
|
background-color: ${(props) => props.theme.input.bg};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Wrapper;
|
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import { useTheme } from 'providers/Theme';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import SingleLineEditor from 'components/SingleLineEditor';
|
||||||
|
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||||
|
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const WsseAuth = ({ collection }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { storedTheme } = useTheme();
|
||||||
|
|
||||||
|
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
|
||||||
|
|
||||||
|
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||||
|
|
||||||
|
const handleUserChange = (username) => {
|
||||||
|
dispatch(
|
||||||
|
updateCollectionAuth({
|
||||||
|
mode: 'wsse',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
content: {
|
||||||
|
username,
|
||||||
|
password: wsseAuth.password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = (password) => {
|
||||||
|
dispatch(
|
||||||
|
updateCollectionAuth({
|
||||||
|
mode: 'wsse',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
content: {
|
||||||
|
username: wsseAuth.username,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="mt-2 w-full">
|
||||||
|
<label className="block font-medium mb-2">Username</label>
|
||||||
|
<div className="single-line-editor-wrapper mb-2">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={wsseAuth.username || ''}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handleUserChange(val)}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="block font-medium mb-2">Password</label>
|
||||||
|
<div className="single-line-editor-wrapper">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={wsseAuth.password || ''}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handlePasswordChange(val)}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WsseAuth;
|
@ -6,6 +6,7 @@ import AwsV4Auth from './AwsV4Auth';
|
|||||||
import BearerAuth from './BearerAuth';
|
import BearerAuth from './BearerAuth';
|
||||||
import BasicAuth from './BasicAuth';
|
import BasicAuth from './BasicAuth';
|
||||||
import DigestAuth from './DigestAuth';
|
import DigestAuth from './DigestAuth';
|
||||||
|
import WsseAuth from './WsseAuth';
|
||||||
import ApiKeyAuth from './ApiKeyAuth/';
|
import ApiKeyAuth from './ApiKeyAuth/';
|
||||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
@ -34,6 +35,9 @@ const Auth = ({ collection }) => {
|
|||||||
case 'oauth2': {
|
case 'oauth2': {
|
||||||
return <OAuth2 collection={collection} />;
|
return <OAuth2 collection={collection} />;
|
||||||
}
|
}
|
||||||
|
case 'wsse': {
|
||||||
|
return <WsseAuth collection={collection} />;
|
||||||
|
}
|
||||||
case 'apikey': {
|
case 'apikey': {
|
||||||
return <ApiKeyAuth collection={collection} />;
|
return <ApiKeyAuth collection={collection} />;
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,6 @@ const VarsTable = ({ collection, vars, varType }) => {
|
|||||||
<td>
|
<td>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span>Value</span>
|
<span>Value</span>
|
||||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
|
@ -88,7 +88,9 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formik.dirty) {
|
if (formik.dirty) {
|
||||||
addButtonRef.current?.scrollIntoView({ behavior: 'smooth' });
|
// Smooth scrolling to the changed parameter is temporarily disabled
|
||||||
|
// due to UX issues when editing the first row in a long list of environment variables.
|
||||||
|
// addButtonRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
}, [formik.values, formik.dirty]);
|
}, [formik.values, formik.dirty]);
|
||||||
|
|
||||||
|
@ -22,6 +22,9 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
|||||||
.required('name is required')
|
.required('name is required')
|
||||||
}),
|
}),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
|
if (values.name === environment.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
|
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('Environment renamed successfully');
|
toast.success('Environment renamed successfully');
|
||||||
|
@ -82,7 +82,6 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
|
|||||||
<td>
|
<td>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span>Value</span>
|
<span>Value</span>
|
||||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
|
@ -39,7 +39,7 @@ const Font = ({ close }) => {
|
|||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<div className="flex flex-row gap-2 w-full">
|
<div className="flex flex-row gap-2 w-full">
|
||||||
<div className="w-4/5">
|
<div className="w-4/5">
|
||||||
<label className="block font-medium">Code Editor Font</label>
|
<label className="block">Code Editor Font</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="block textbox mt-2 w-full"
|
className="block textbox mt-2 w-full"
|
||||||
@ -52,7 +52,7 @@ const Font = ({ close }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/5">
|
<div className="w-1/5">
|
||||||
<label className="block font-medium">Font Size</label>
|
<label className="block">Font Size</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="block textbox mt-2 w-full"
|
className="block textbox mt-2 w-full"
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Font from './Font/index';
|
||||||
|
import Theme from './Theme/index';
|
||||||
|
|
||||||
|
const Display = ({ close }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col my-2 gap-10 w-full">
|
||||||
|
<div className='w-full flex flex-col gap-2'>
|
||||||
|
<span>
|
||||||
|
Theme
|
||||||
|
</span>
|
||||||
|
<Theme close={close} />
|
||||||
|
</div>
|
||||||
|
<div className='h-[1px] bg-[#aaa5] w-full'></div>
|
||||||
|
<div className='w-fit flex flex-col gap-2'>
|
||||||
|
<Font close={close} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Display;
|
@ -100,7 +100,7 @@ const General = ({ close }) => {
|
|||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||||
<div className="flex items-center mt-2">
|
<div className="flex items-center my-2">
|
||||||
<input
|
<input
|
||||||
id="sslVerification"
|
id="sslVerification"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -113,7 +113,7 @@ const ProxySettings = ({ close }) => {
|
|||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<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 mt-2">
|
||||||
<label className="settings-label" htmlFor="protocol">
|
<label className="settings-label" htmlFor="protocol">
|
||||||
Mode
|
Mode
|
||||||
</label>
|
</label>
|
||||||
|
@ -2,13 +2,12 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
const StyledWrapper = styled.div`
|
const StyledWrapper = styled.div`
|
||||||
div.tabs {
|
div.tabs {
|
||||||
margin-top: -0.5rem;
|
|
||||||
|
|
||||||
div.tab {
|
div.tab {
|
||||||
padding: 6px 0px;
|
width: 100%;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 7px 10px;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: solid 2px transparent;
|
border-bottom: solid 2px transparent;
|
||||||
margin-right: 1.25rem;
|
|
||||||
color: var(--color-tab-inactive);
|
color: var(--color-tab-inactive);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@ -22,8 +21,12 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
color: ${(props) => props.theme.sidebar.color} !important;
|
||||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
background: ${(props) => props.theme.sidebar.collection.item.bg};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${(props) => props.theme.sidebar.collection.item.bg} !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,9 @@ import classnames from 'classnames';
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Support from './Support';
|
import Support from './Support';
|
||||||
import General from './General';
|
import General from './General';
|
||||||
import Font from './Font';
|
|
||||||
import Theme from './Theme';
|
|
||||||
import Proxy from './ProxySettings';
|
import Proxy from './ProxySettings';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import Display from './Display/index';
|
||||||
|
|
||||||
const Preferences = ({ onClose }) => {
|
const Preferences = ({ onClose }) => {
|
||||||
const [tab, setTab] = useState('general');
|
const [tab, setTab] = useState('general');
|
||||||
@ -27,41 +26,36 @@ const Preferences = ({ onClose }) => {
|
|||||||
return <Proxy close={onClose} />;
|
return <Proxy close={onClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'theme': {
|
case 'display': {
|
||||||
return <Theme close={onClose} />;
|
return <Display close={onClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'support': {
|
case 'support': {
|
||||||
return <Support />;
|
return <Support />;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'font': {
|
|
||||||
return <Font close={onClose} />;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<Modal size="lg" title="Preferences" handleCancel={onClose} hideFooter={true}>
|
<Modal size="lg" title="Preferences" handleCancel={onClose} hideFooter={true}>
|
||||||
<div className="flex items-center px-2 tabs" role="tablist">
|
<div className='flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem]'>
|
||||||
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
|
<div className="flex flex-col items-center tabs" role="tablist">
|
||||||
General
|
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
|
||||||
</div>
|
General
|
||||||
<div className={getTabClassname('theme')} role="tab" onClick={() => setTab('theme')}>
|
</div>
|
||||||
Theme
|
<div className={getTabClassname('display')} role="tab" onClick={() => setTab('display')}>
|
||||||
</div>
|
Display
|
||||||
<div className={getTabClassname('font')} role="tab" onClick={() => setTab('font')}>
|
</div>
|
||||||
Font
|
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||||
</div>
|
Proxy
|
||||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
</div>
|
||||||
Proxy
|
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
||||||
</div>
|
Support
|
||||||
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
</div>
|
||||||
Support
|
|
||||||
</div>
|
</div>
|
||||||
|
<section className="flex flex-grow px-2 pt-2 pb-6 tab-panel">{getTabPanel(tab)}</section>
|
||||||
</div>
|
</div>
|
||||||
<section className="flex flex-grow px-2 mt-4 tab-panel">{getTabPanel(tab)}</section>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
|
@ -30,7 +30,6 @@ const AuthMode = ({ item, collection }) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||||
@ -80,6 +79,15 @@ const AuthMode = ({ item, collection }) => {
|
|||||||
>
|
>
|
||||||
OAuth 2.0
|
OAuth 2.0
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => {
|
||||||
|
dropdownTippyRef?.current?.hide();
|
||||||
|
onModeChange('wsse');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
WSSE Auth
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
label {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-line-editor-wrapper {
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: solid 1px ${(props) => props.theme.input.border};
|
||||||
|
background-color: ${(props) => props.theme.input.bg};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Wrapper;
|
@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import { useTheme } from 'providers/Theme';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import SingleLineEditor from 'components/SingleLineEditor';
|
||||||
|
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||||
|
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const WsseAuth = ({ item, collection }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { storedTheme } = useTheme();
|
||||||
|
|
||||||
|
const wsseAuth = item.draft ? get(item, 'draft.request.auth.wsse', {}) : get(item, 'request.auth.wsse', {});
|
||||||
|
|
||||||
|
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||||
|
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||||
|
|
||||||
|
const handleUserChange = (username) => {
|
||||||
|
dispatch(
|
||||||
|
updateAuth({
|
||||||
|
mode: 'wsse',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
itemUid: item.uid,
|
||||||
|
content: {
|
||||||
|
username,
|
||||||
|
password: wsseAuth.password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = (password) => {
|
||||||
|
dispatch(
|
||||||
|
updateAuth({
|
||||||
|
mode: 'wsse',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
itemUid: item.uid,
|
||||||
|
content: {
|
||||||
|
username: wsseAuth.username,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="mt-2 w-full">
|
||||||
|
<label className="block font-medium mb-2">Username</label>
|
||||||
|
<div className="single-line-editor-wrapper mb-2">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={wsseAuth.username || ''}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handleUserChange(val)}
|
||||||
|
onRun={handleRun}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="block font-medium mb-2">Password</label>
|
||||||
|
<div className="single-line-editor-wrapper">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={wsseAuth.password || ''}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handlePasswordChange(val)}
|
||||||
|
onRun={handleRun}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WsseAuth;
|
@ -5,6 +5,7 @@ import AwsV4Auth from './AwsV4Auth';
|
|||||||
import BearerAuth from './BearerAuth';
|
import BearerAuth from './BearerAuth';
|
||||||
import BasicAuth from './BasicAuth';
|
import BasicAuth from './BasicAuth';
|
||||||
import DigestAuth from './DigestAuth';
|
import DigestAuth from './DigestAuth';
|
||||||
|
import WsseAuth from './WsseAuth';
|
||||||
import ApiKeyAuth from './ApiKeyAuth';
|
import ApiKeyAuth from './ApiKeyAuth';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { humanizeRequestAuthMode } from 'utils/collections/index';
|
import { humanizeRequestAuthMode } from 'utils/collections/index';
|
||||||
@ -33,6 +34,9 @@ const Auth = ({ item, collection }) => {
|
|||||||
case 'oauth2': {
|
case 'oauth2': {
|
||||||
return <OAuth2 collection={collection} item={item} />;
|
return <OAuth2 collection={collection} item={item} />;
|
||||||
}
|
}
|
||||||
|
case 'wsse': {
|
||||||
|
return <WsseAuth collection={collection} item={item} />;
|
||||||
|
}
|
||||||
case 'apikey': {
|
case 'apikey': {
|
||||||
return <ApiKeyAuth collection={collection} item={item} />;
|
return <ApiKeyAuth collection={collection} item={item} />;
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,6 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
|||||||
<td>
|
<td>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span>Value</span>
|
<span>Value</span>
|
||||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
|
@ -18,6 +18,7 @@ import CloneCollectionItem from 'components/Sidebar/Collections/Collection/Colle
|
|||||||
import NewRequest from 'components/Sidebar/NewRequest/index';
|
import NewRequest from 'components/Sidebar/NewRequest/index';
|
||||||
import CloseTabIcon from './CloseTabIcon';
|
import CloseTabIcon from './CloseTabIcon';
|
||||||
import DraftTabIcon from './DraftTabIcon';
|
import DraftTabIcon from './DraftTabIcon';
|
||||||
|
import { flattenItems } from 'utils/collections/index';
|
||||||
|
|
||||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
|
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -282,11 +283,10 @@ function RequestTabMenu({ onDropdownCreate, onCloseTabs, collectionRequestTabs,
|
|||||||
function handleCloseSavedTabs(event) {
|
function handleCloseSavedTabs(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const savedTabs = collectionRequestTabs.filter((tab) => {
|
const items = flattenItems(collection?.items);
|
||||||
const item = findItemInCollection(collection, tab.uid)
|
const savedTabs = items?.filter?.((item) => !item.draft);
|
||||||
return item && !item.draft;
|
const savedTabIds = savedTabs?.map((item) => item.uid) || [];
|
||||||
});
|
onCloseTabs(savedTabIds);
|
||||||
onCloseTabs(savedTabs.map((tab) => tab.uid));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCloseAllTabs(event) {
|
function handleCloseAllTabs(event) {
|
||||||
|
@ -15,7 +15,7 @@ import StyledWrapper from './StyledWrapper';
|
|||||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||||
const [selectedTab, setSelectedTab] = useState('response');
|
const [selectedTab, setSelectedTab] = useState('response');
|
||||||
|
|
||||||
const { requestSent, responseReceived, testResults, assertionResults } = item;
|
const { requestSent, responseReceived, testResults, assertionResults, error } = item;
|
||||||
|
|
||||||
const headers = get(item, 'responseReceived.headers', []);
|
const headers = get(item, 'responseReceived.headers', []);
|
||||||
const status = get(item, 'responseReceived.status', 0);
|
const status = get(item, 'responseReceived.status', 0);
|
||||||
@ -36,6 +36,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
data={responseReceived.data}
|
data={responseReceived.data}
|
||||||
dataBuffer={responseReceived.dataBuffer}
|
dataBuffer={responseReceived.dataBuffer}
|
||||||
headers={responseReceived.headers}
|
headers={responseReceived.headers}
|
||||||
|
error={error}
|
||||||
key={item.filename}
|
key={item.filename}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -32,7 +32,10 @@ const CodeView = ({ language, item }) => {
|
|||||||
|
|
||||||
let snippet = '';
|
let snippet = '';
|
||||||
try {
|
try {
|
||||||
snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers })).convert(target, client);
|
snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers, type: item.type })).convert(
|
||||||
|
target,
|
||||||
|
client
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
snippet = 'Error generating code snippet';
|
snippet = 'Error generating code snippet';
|
||||||
|
@ -28,9 +28,12 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
|||||||
if (!isFolder && item.draft) {
|
if (!isFolder && item.draft) {
|
||||||
await dispatch(saveRequest(item.uid, collection.uid, true));
|
await dispatch(saveRequest(item.uid, collection.uid, true));
|
||||||
}
|
}
|
||||||
|
if (item.name === values.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatch(renameItem(values.name, item.uid, collection.uid))
|
dispatch(renameItem(values.name, item.uid, collection.uid))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('Request renamed!');
|
toast.success('Request renamed');
|
||||||
onClose();
|
onClose();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -55,7 +58,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
|||||||
handleConfirm={onSubmit}
|
handleConfirm={onSubmit}
|
||||||
handleCancel={onClose}
|
handleCancel={onClose}
|
||||||
>
|
>
|
||||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="block font-semibold">
|
<label htmlFor="name" className="block font-semibold">
|
||||||
{isFolder ? 'Folder' : 'Request'} Name
|
{isFolder ? 'Folder' : 'Request'} Name
|
||||||
|
@ -349,7 +349,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
Run
|
Run
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isFolder && item.type === 'http-request' && (
|
{!isFolder && (item.type === 'http-request' || item.type === 'graphql-request') && (
|
||||||
<div
|
<div
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -185,7 +185,7 @@ const Sidebar = () => {
|
|||||||
Star
|
Star
|
||||||
</GitHubButton> */}
|
</GitHubButton> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.30.0</div>
|
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.30.1</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,25 +1,23 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
const StopWatch = ({ requestTimestamp }) => {
|
const StopWatch = () => {
|
||||||
const [milliseconds, setMilliseconds] = useState(0);
|
const [milliseconds, setMilliseconds] = useState(0);
|
||||||
|
|
||||||
const tickInterval = 200;
|
const tickInterval = 100;
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
setMilliseconds(milliseconds + tickInterval);
|
setMilliseconds(_milliseconds => _milliseconds + tickInterval);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timerID = setInterval(() => tick(), tickInterval);
|
let timerID = setInterval(() => {
|
||||||
|
tick()
|
||||||
|
}, tickInterval);
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(timerID);
|
clearTimeout(timerID);
|
||||||
};
|
};
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
if (milliseconds < 250) {
|
||||||
setMilliseconds(Date.now() - requestTimestamp);
|
|
||||||
}, [requestTimestamp]);
|
|
||||||
|
|
||||||
if (milliseconds < 1000) {
|
|
||||||
return 'Loading...';
|
return 'Loading...';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,4 +25,4 @@ const StopWatch = ({ requestTimestamp }) => {
|
|||||||
return <span>{seconds.toFixed(1)}s</span>;
|
return <span>{seconds.toFixed(1)}s</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StopWatch;
|
export default React.memo(StopWatch);
|
||||||
|
@ -21,9 +21,7 @@ const Welcome = () => {
|
|||||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||||
|
|
||||||
const handleOpenCollection = () => {
|
const handleOpenCollection = () => {
|
||||||
dispatch(openCollection()).catch(
|
dispatch(openCollection()).catch((err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR')));
|
||||||
(err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR'))
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportCollection = ({ collection, translationLog }) => {
|
const handleImportCollection = ({ collection, translationLog }) => {
|
||||||
@ -64,7 +62,7 @@ const Welcome = () => {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div>
|
<div aria-hidden className="">
|
||||||
<Bruno width={50} />
|
<Bruno width={50} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-semibold select-none">bruno</div>
|
<div className="text-xl font-semibold select-none">bruno</div>
|
||||||
@ -72,40 +70,69 @@ const Welcome = () => {
|
|||||||
|
|
||||||
<div className="uppercase font-semibold heading mt-10">{t('COMMON.COLLECTIONS')}</div>
|
<div className="uppercase font-semibold heading mt-10">{t('COMMON.COLLECTIONS')}</div>
|
||||||
<div className="mt-4 flex items-center collection-options select-none">
|
<div className="mt-4 flex items-center collection-options select-none">
|
||||||
<div className="flex items-center" onClick={() => setCreateCollectionModalOpen(true)}>
|
<button
|
||||||
<IconPlus size={18} strokeWidth={2} />
|
className="flex items-center"
|
||||||
|
onClick={() => setCreateCollectionModalOpen(true)}
|
||||||
|
aria-label={t('WELCOME.CREATE_COLLECTION')}
|
||||||
|
>
|
||||||
|
<IconPlus aria-hidden size={18} strokeWidth={2} />
|
||||||
<span className="label ml-2" id="create-collection">
|
<span className="label ml-2" id="create-collection">
|
||||||
{t('WELCOME.CREATE_COLLECTION')}
|
{t('WELCOME.CREATE_COLLECTION')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
<div className="flex items-center ml-6" onClick={handleOpenCollection}>
|
|
||||||
<IconFolders size={18} strokeWidth={2} />
|
<button className="flex items-center ml-6" onClick={handleOpenCollection} aria-label="Open Collection">
|
||||||
|
<IconFolders aria-hidden size={18} strokeWidth={2} />
|
||||||
<span className="label ml-2">{t('WELCOME.OPEN_COLLECTION')}</span>
|
<span className="label ml-2">{t('WELCOME.OPEN_COLLECTION')}</span>
|
||||||
</div>
|
</button>
|
||||||
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
|
|
||||||
<IconDownload size={18} strokeWidth={2} />
|
<button
|
||||||
|
className="flex items-center ml-6"
|
||||||
|
onClick={() => setImportCollectionModalOpen(true)}
|
||||||
|
aria-label={t('WELCOME.IMPORT_COLLECTION')}
|
||||||
|
>
|
||||||
|
<IconDownload aria-hidden size={18} strokeWidth={2} />
|
||||||
<span className="label ml-2" id="import-collection">
|
<span className="label ml-2" id="import-collection">
|
||||||
{t('WELCOME.IMPORT_COLLECTION')}
|
{t('WELCOME.IMPORT_COLLECTION')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="uppercase font-semibold heading mt-10 pt-6">{t('WELCOME.LINKS')}</div>
|
<div className="uppercase font-semibold heading mt-10 pt-6">{t('WELCOME.LINKS')}</div>
|
||||||
<div className="mt-4 flex flex-col collection-options select-none">
|
<div className="mt-4 flex flex-col collection-options select-none">
|
||||||
<div className="flex items-center mt-2">
|
<div className="flex items-center mt-2">
|
||||||
<a href="https://docs.usebruno.com" target="_blank" className="inline-flex items-center">
|
<a
|
||||||
<IconBook size={18} strokeWidth={2} />
|
href="https://docs.usebruno.com"
|
||||||
|
aria-label="Read documentation"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center"
|
||||||
|
>
|
||||||
|
<IconBook aria-hidden size={18} strokeWidth={2} />
|
||||||
<span className="label ml-2">{t('COMMON.DOCUMENTATION')}</span>
|
<span className="label ml-2">{t('COMMON.DOCUMENTATION')}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center mt-2">
|
<div className="flex items-center mt-2">
|
||||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
|
<a
|
||||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
href="https://github.com/usebruno/bruno/issues"
|
||||||
|
aria-label="Report issues on GitHub"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center"
|
||||||
|
>
|
||||||
|
<IconSpeakerphone aria-hidden size={18} strokeWidth={2} />
|
||||||
<span className="label ml-2">{t('COMMON.REPORT_ISSUES')}</span>
|
<span className="label ml-2">{t('COMMON.REPORT_ISSUES')}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center mt-2">
|
<div className="flex items-center mt-2">
|
||||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
|
<a
|
||||||
<IconBrandGithub size={18} strokeWidth={2} />
|
href="https://github.com/usebruno/bruno"
|
||||||
|
aria-label="Go to GitHub repository"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
<IconBrandGithub aria-hidden size={18} strokeWidth={2} />
|
||||||
<span className="label ml-2">{t('COMMON.GITHUB')}</span>
|
<span className="label ml-2">{t('COMMON.GITHUB')}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,7 +60,7 @@ const trackStart = () => {
|
|||||||
event: 'start',
|
event: 'start',
|
||||||
properties: {
|
properties: {
|
||||||
os: platformLib.os.family,
|
os: platformLib.os.family,
|
||||||
version: '1.30.0'
|
version: '1.30.1'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -43,6 +43,7 @@ import { resolveRequestFilename } from 'utils/common/platform';
|
|||||||
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
|
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||||
import { name } from 'file-loader';
|
import { name } from 'file-loader';
|
||||||
|
import slash from 'utils/common/slash';
|
||||||
|
|
||||||
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
@ -401,7 +402,7 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
|||||||
}
|
}
|
||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
|
ipcRenderer.invoke('renderer:rename-item', slash(item.pathname), newPathname, newName).then(resolve).catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -477,6 +477,10 @@ export const collectionsSlice = createSlice({
|
|||||||
item.draft.request.auth.mode = 'oauth2';
|
item.draft.request.auth.mode = 'oauth2';
|
||||||
item.draft.request.auth.oauth2 = action.payload.content;
|
item.draft.request.auth.oauth2 = action.payload.content;
|
||||||
break;
|
break;
|
||||||
|
case 'wsse':
|
||||||
|
item.draft.request.auth.mode = 'wsse';
|
||||||
|
item.draft.request.auth.wsse = action.payload.content;
|
||||||
|
break;
|
||||||
case 'apikey':
|
case 'apikey':
|
||||||
item.draft.request.auth.mode = 'apikey';
|
item.draft.request.auth.mode = 'apikey';
|
||||||
item.draft.request.auth.apikey = action.payload.content;
|
item.draft.request.auth.apikey = action.payload.content;
|
||||||
@ -1141,6 +1145,9 @@ export const collectionsSlice = createSlice({
|
|||||||
case 'oauth2':
|
case 'oauth2':
|
||||||
set(collection, 'root.request.auth.oauth2', action.payload.content);
|
set(collection, 'root.request.auth.oauth2', action.payload.content);
|
||||||
break;
|
break;
|
||||||
|
case 'wsse':
|
||||||
|
set(collection, 'root.request.auth.wsse', action.payload.content);
|
||||||
|
break;
|
||||||
case 'apikey':
|
case 'apikey':
|
||||||
set(collection, 'root.request.auth.apikey', action.payload.content);
|
set(collection, 'root.request.auth.apikey', action.payload.content);
|
||||||
break;
|
break;
|
||||||
|
@ -31,6 +31,7 @@ const createHeaders = (request, headers) => {
|
|||||||
if (contentType !== '') {
|
if (contentType !== '') {
|
||||||
enabledHeaders.push({ name: 'content-type', value: contentType });
|
enabledHeaders.push({ name: 'content-type', value: contentType });
|
||||||
}
|
}
|
||||||
|
|
||||||
return enabledHeaders;
|
return enabledHeaders;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -43,7 +44,14 @@ const createQuery = (queryParams = []) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPostData = (body) => {
|
const createPostData = (body, type) => {
|
||||||
|
if (type === 'graphql-request') {
|
||||||
|
return {
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(body[body.mode])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const contentType = createContentType(body.mode);
|
const contentType = createContentType(body.mode);
|
||||||
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
|
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
|
||||||
return {
|
return {
|
||||||
@ -64,7 +72,7 @@ const createPostData = (body) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildHarRequest = ({ request, headers }) => {
|
export const buildHarRequest = ({ request, headers, type }) => {
|
||||||
return {
|
return {
|
||||||
method: request.method,
|
method: request.method,
|
||||||
url: encodeURI(request.url),
|
url: encodeURI(request.url),
|
||||||
@ -72,7 +80,7 @@ export const buildHarRequest = ({ request, headers }) => {
|
|||||||
cookies: [],
|
cookies: [],
|
||||||
headers: createHeaders(request, headers),
|
headers: createHeaders(request, headers),
|
||||||
queryString: createQuery(request.params),
|
queryString: createQuery(request.params),
|
||||||
postData: createPostData(request.body),
|
postData: createPostData(request.body, type),
|
||||||
headersSize: 0,
|
headersSize: 0,
|
||||||
bodySize: 0
|
bodySize: 0
|
||||||
};
|
};
|
||||||
|
@ -379,7 +379,12 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
|||||||
placement: get(si.request, 'auth.apikey.placement', 'header')
|
placement: get(si.request, 'auth.apikey.placement', 'header')
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case 'wsse':
|
||||||
|
di.request.auth.wsse = {
|
||||||
|
username: get(si.request, 'auth.wsse.username', ''),
|
||||||
|
password: get(si.request, 'auth.wsse.password', '')
|
||||||
|
};
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -669,6 +674,10 @@ export const humanizeRequestAuthMode = (mode) => {
|
|||||||
label = 'OAuth 2.0';
|
label = 'OAuth 2.0';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'wsse': {
|
||||||
|
label = 'WSSE Auth';
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'apikey': {
|
case 'apikey': {
|
||||||
label = 'API Key';
|
label = 'API Key';
|
||||||
break;
|
break;
|
||||||
|
@ -174,7 +174,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
|
|||||||
} else if (mimeType === 'text/plain') {
|
} else if (mimeType === 'text/plain') {
|
||||||
brunoRequestItem.request.body.mode = 'text';
|
brunoRequestItem.request.body.mode = 'text';
|
||||||
brunoRequestItem.request.body.text = request.body.text;
|
brunoRequestItem.request.body.text = request.body.text;
|
||||||
} else if (mimeType === 'text/xml') {
|
} else if (mimeType === 'text/xml' || mimeType === 'application/xml') {
|
||||||
brunoRequestItem.request.body.mode = 'xml';
|
brunoRequestItem.request.body.mode = 'xml';
|
||||||
brunoRequestItem.request.body.xml = request.body.text;
|
brunoRequestItem.request.body.xml = request.body.text;
|
||||||
} else if (mimeType === 'application/graphql') {
|
} else if (mimeType === 'application/graphql') {
|
||||||
|
@ -32,7 +32,7 @@ const readFile = (files) => {
|
|||||||
|
|
||||||
const ensureUrl = (url) => {
|
const ensureUrl = (url) => {
|
||||||
// emoving multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
|
// emoving multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
|
||||||
return url.replace(/(^\w+:|^)\/{2,}/, '$1/');
|
return url.replace(/([^:])\/{2,}/g, '$1/');
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildEmptyJsonBody = (bodySchema) => {
|
const buildEmptyJsonBody = (bodySchema) => {
|
||||||
|
@ -54,6 +54,47 @@ const convertV21Auth = (array) => {
|
|||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const constructUrlFromParts = (url) => {
|
||||||
|
const { protocol = 'http', host, path, port, query, hash } = url || {};
|
||||||
|
const hostStr = Array.isArray(host) ? host.filter(Boolean).join('.') : host || '';
|
||||||
|
const pathStr = Array.isArray(path) ? path.filter(Boolean).join('/') : path || '';
|
||||||
|
const portStr = port ? `:${port}` : '';
|
||||||
|
const queryStr =
|
||||||
|
query && Array.isArray(query) && query.length > 0
|
||||||
|
? `?${query
|
||||||
|
.filter((q) => q.key)
|
||||||
|
.map((q) => `${q.key}=${q.value || ''}`)
|
||||||
|
.join('&')}`
|
||||||
|
: '';
|
||||||
|
const urlStr = `${protocol}://${hostStr}${portStr}${pathStr ? `/${pathStr}` : ''}${queryStr}`;
|
||||||
|
return urlStr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const constructUrl = (url) => {
|
||||||
|
if (!url) return '';
|
||||||
|
|
||||||
|
if (typeof url === 'string') {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof url === 'object') {
|
||||||
|
const { raw } = url;
|
||||||
|
|
||||||
|
if (raw && typeof raw === 'string') {
|
||||||
|
// If the raw URL contains url-fragments remove it
|
||||||
|
if (raw.includes('#')) {
|
||||||
|
return raw.split('#')[0]; // Returns the part of raw URL without the url-fragment part.
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no raw value exists, construct the URL from parts
|
||||||
|
return constructUrlFromParts(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
let translationLog = {};
|
let translationLog = {};
|
||||||
|
|
||||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
|
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
|
||||||
@ -94,12 +135,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = '';
|
const url = constructUrl(i.request.url);
|
||||||
if (typeof i.request.url === 'string') {
|
|
||||||
url = i.request.url;
|
|
||||||
} else {
|
|
||||||
url = get(i, 'request.url.raw') || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const brunoRequestItem = {
|
const brunoRequestItem = {
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
@ -107,7 +143,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
|||||||
type: 'http-request',
|
type: 'http-request',
|
||||||
request: {
|
request: {
|
||||||
url: url,
|
url: url,
|
||||||
method: i.request.method,
|
method: i?.request?.method?.toUpperCase(),
|
||||||
auth: {
|
auth: {
|
||||||
mode: 'none',
|
mode: 'none',
|
||||||
basic: null,
|
basic: null,
|
||||||
@ -313,12 +349,17 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
each(get(i, 'request.url.variable'), (param) => {
|
each(get(i, 'request.url.variable', []), (param) => {
|
||||||
|
if (!param.key) {
|
||||||
|
// If no key, skip this iteration and discard the param
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
brunoRequestItem.request.params.push({
|
brunoRequestItem.request.params.push({
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
name: param.key,
|
name: param.key,
|
||||||
value: param.value,
|
value: param.value ?? '',
|
||||||
description: param.description,
|
description: param.description ?? '',
|
||||||
type: 'path',
|
type: 'path',
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const { interpolate } = require('@usebruno/common');
|
const { interpolate } = require('@usebruno/common');
|
||||||
const { each, forOwn, cloneDeep, find } = require('lodash');
|
const { each, forOwn, cloneDeep, find } = require('lodash');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
|
||||||
const getContentType = (headers = {}) => {
|
const getContentType = (headers = {}) => {
|
||||||
let contentType = '';
|
let contentType = '';
|
||||||
@ -78,6 +79,14 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
|
|||||||
request.data = JSON.parse(parsed);
|
request.data = JSON.parse(parsed);
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
|
} else if (contentType === 'multipart/form-data') {
|
||||||
|
if (typeof request.data === 'object' && !(request?.data instanceof FormData)) {
|
||||||
|
try {
|
||||||
|
let parsed = JSON.stringify(request.data);
|
||||||
|
parsed = _interpolate(parsed);
|
||||||
|
request.data = JSON.parse(parsed);
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
request.data = _interpolate(request.data);
|
request.data = _interpolate(request.data);
|
||||||
}
|
}
|
||||||
@ -113,7 +122,8 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
|
|||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
request.url = url.origin + interpolatedUrlPath + url.search;
|
const trailingSlash = url.pathname.endsWith('/') ? '/' : '';
|
||||||
|
request.url = url.origin + interpolatedUrlPath + trailingSlash + url.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.proxy) {
|
if (request.proxy) {
|
||||||
|
@ -2,6 +2,7 @@ const { get, each, filter } = require('lodash');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
var JSONbig = require('json-bigint');
|
var JSONbig = require('json-bigint');
|
||||||
const decomment = require('decomment');
|
const decomment = require('decomment');
|
||||||
|
const crypto = require('node:crypto');
|
||||||
|
|
||||||
const prepareRequest = (request, collectionRoot) => {
|
const prepareRequest = (request, collectionRoot) => {
|
||||||
const headers = {};
|
const headers = {};
|
||||||
@ -69,6 +70,24 @@ const prepareRequest = (request, collectionRoot) => {
|
|||||||
if (request.auth.mode === 'bearer') {
|
if (request.auth.mode === 'bearer') {
|
||||||
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.auth.mode === 'wsse') {
|
||||||
|
const username = get(request, 'auth.wsse.username', '');
|
||||||
|
const password = get(request, 'auth.wsse.password', '');
|
||||||
|
|
||||||
|
const ts = new Date().toISOString();
|
||||||
|
const nonce = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
|
// Create the password digest using SHA-256
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.update(nonce + ts + password);
|
||||||
|
const digest = hash.digest('base64');
|
||||||
|
|
||||||
|
// Construct the WSSE header
|
||||||
|
axiosRequest.headers[
|
||||||
|
'X-WSSE'
|
||||||
|
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.body = request.body || {};
|
request.body = request.body || {};
|
||||||
@ -120,16 +139,10 @@ const prepareRequest = (request, collectionRoot) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (request.body.mode === 'multipartForm') {
|
if (request.body.mode === 'multipartForm') {
|
||||||
|
axiosRequest.headers['content-type'] = 'multipart/form-data';
|
||||||
const params = {};
|
const params = {};
|
||||||
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
|
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
|
||||||
each(enabledParams, (p) => {
|
each(enabledParams, (p) => (params[p.name] = p.value));
|
||||||
if (p.type === 'file') {
|
|
||||||
params[p.name] = p.value.map((path) => fs.createReadStream(path));
|
|
||||||
} else {
|
|
||||||
params[p.name] = p.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
axiosRequest.headers['content-type'] = 'multipart/form-data';
|
|
||||||
axiosRequest.data = params;
|
axiosRequest.data = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ const { makeAxiosInstance } = require('../utils/axios-instance');
|
|||||||
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
|
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
|
||||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
|
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { createFormData } = require('../utils/common');
|
||||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||||
|
|
||||||
const onConsoleLog = (type, args) => {
|
const onConsoleLog = (type, args) => {
|
||||||
@ -42,24 +43,11 @@ const runSingleRequest = async function (
|
|||||||
|
|
||||||
request = prepareRequest(bruJson.request, collectionRoot);
|
request = prepareRequest(bruJson.request, collectionRoot);
|
||||||
|
|
||||||
|
request.__bruno__executionMode = 'cli';
|
||||||
|
|
||||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||||
scriptingConfig.runtime = runtime;
|
scriptingConfig.runtime = runtime;
|
||||||
|
|
||||||
// make axios work in node using form data
|
|
||||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
|
||||||
if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
|
|
||||||
const form = new FormData();
|
|
||||||
forOwn(request.data, (value, key) => {
|
|
||||||
if (value instanceof Array) {
|
|
||||||
each(value, (v) => form.append(key, v));
|
|
||||||
} else {
|
|
||||||
form.append(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
extend(request.headers, form.getHeaders());
|
|
||||||
request.data = form;
|
|
||||||
}
|
|
||||||
|
|
||||||
// run pre request script
|
// run pre request script
|
||||||
const requestScriptFile = compact([
|
const requestScriptFile = compact([
|
||||||
get(collectionRoot, 'request.script.req'),
|
get(collectionRoot, 'request.script.req'),
|
||||||
@ -195,6 +183,14 @@ const runSingleRequest = async function (
|
|||||||
request.data = qs.stringify(request.data);
|
request.data = qs.stringify(request.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request?.headers?.['content-type'] === 'multipart/form-data') {
|
||||||
|
if (!(request?.data instanceof FormData)) {
|
||||||
|
let form = createFormData(request.data, collectionPath);
|
||||||
|
request.data = form;
|
||||||
|
extend(request.headers, form.getHeaders());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let response, responseTime;
|
let response, responseTime;
|
||||||
try {
|
try {
|
||||||
// run request
|
// run request
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const { forOwn } = require('lodash');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
const lpad = (str, width) => {
|
const lpad = (str, width) => {
|
||||||
let paddedStr = str;
|
let paddedStr = str;
|
||||||
while (paddedStr.length < width) {
|
while (paddedStr.length < width) {
|
||||||
@ -14,7 +19,33 @@ const rpad = (str, width) => {
|
|||||||
return paddedStr;
|
return paddedStr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createFormData = (datas, collectionPath) => {
|
||||||
|
// make axios work in node using form data
|
||||||
|
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||||
|
const form = new FormData();
|
||||||
|
forOwn(datas, (value, key) => {
|
||||||
|
if (typeof value == 'string') {
|
||||||
|
form.append(key, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePaths = value || [];
|
||||||
|
filePaths?.forEach?.((filePath) => {
|
||||||
|
let trimmedFilePath = filePath.trim();
|
||||||
|
|
||||||
|
if (!path.isAbsolute(trimmedFilePath)) {
|
||||||
|
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return form;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
lpad,
|
lpad,
|
||||||
rpad
|
rpad,
|
||||||
|
createFormData
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "v1.30.0",
|
"version": "v1.30.1",
|
||||||
"name": "bruno",
|
"name": "bruno",
|
||||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||||
"homepage": "https://www.usebruno.com",
|
"homepage": "https://www.usebruno.com",
|
||||||
|
@ -16,6 +16,8 @@ const {
|
|||||||
sanitizeDirectoryName,
|
sanitizeDirectoryName,
|
||||||
isWSLPath,
|
isWSLPath,
|
||||||
normalizeWslPath,
|
normalizeWslPath,
|
||||||
|
normalizeAndResolvePath,
|
||||||
|
safeToRename
|
||||||
} = require('../utils/filesystem');
|
} = require('../utils/filesystem');
|
||||||
const { openCollectionDialog } = require('../app/collections');
|
const { openCollectionDialog } = require('../app/collections');
|
||||||
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
||||||
@ -296,7 +298,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
|
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
|
||||||
if (fs.existsSync(newEnvFilePath)) {
|
if (!safeToRename(envFilePath, newEnvFilePath)) {
|
||||||
throw new Error(`environment: ${newEnvFilePath} already exists`);
|
throw new Error(`environment: ${newEnvFilePath} already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,21 +331,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
|
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
|
||||||
try {
|
try {
|
||||||
// Normalize paths if they are WSL paths
|
// Normalize paths if they are WSL paths
|
||||||
if (isWSLPath(oldPath)) {
|
oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath);
|
||||||
oldPath = normalizeWslPath(oldPath);
|
newPath = isWSLPath(newPath) ? normalizeWslPath(newPath) : normalizeAndResolvePath(newPath);
|
||||||
}
|
|
||||||
if (isWSLPath(newPath)) {
|
|
||||||
newPath = normalizeWslPath(newPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Check if the old path exists
|
||||||
if (!fs.existsSync(oldPath)) {
|
if (!fs.existsSync(oldPath)) {
|
||||||
throw new Error(`path: ${oldPath} does not exist`);
|
throw new Error(`path: ${oldPath} does not exist`);
|
||||||
}
|
}
|
||||||
if (fs.existsSync(newPath)) {
|
|
||||||
throw new Error(`path: ${oldPath} already exists`);
|
if (!safeToRename(oldPath, newPath)) {
|
||||||
|
throw new Error(`path: ${newPath} already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if its directory, rename and return
|
|
||||||
if (isDirectory(oldPath)) {
|
if (isDirectory(oldPath)) {
|
||||||
const bruFilesAtSource = await searchForBruFiles(oldPath);
|
const bruFilesAtSource = await searchForBruFiles(oldPath);
|
||||||
|
|
||||||
@ -364,12 +363,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
const jsonData = bruToJson(data);
|
const jsonData = bruToJson(data);
|
||||||
|
|
||||||
jsonData.name = newName;
|
jsonData.name = newName;
|
||||||
|
|
||||||
moveRequestUid(oldPath, newPath);
|
moveRequestUid(oldPath, newPath);
|
||||||
|
|
||||||
const content = jsonToBru(jsonData);
|
const content = jsonToBru(jsonData);
|
||||||
await writeFile(newPath, content);
|
|
||||||
await fs.unlinkSync(oldPath);
|
await fs.unlinkSync(oldPath);
|
||||||
|
await writeFile(newPath, content);
|
||||||
|
|
||||||
|
return newPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ const decomment = require('decomment');
|
|||||||
const contentDispositionParser = require('content-disposition');
|
const contentDispositionParser = require('content-disposition');
|
||||||
const mime = require('mime-types');
|
const mime = require('mime-types');
|
||||||
const { ipcMain } = require('electron');
|
const { ipcMain } = require('electron');
|
||||||
const { isUndefined, isNull, each, get, compact, cloneDeep } = require('lodash');
|
const { isUndefined, isNull, each, get, compact, cloneDeep, forOwn, extend } = require('lodash');
|
||||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
||||||
const prepareRequest = require('./prepare-request');
|
const prepareRequest = require('./prepare-request');
|
||||||
const prepareCollectionRequest = require('./prepare-collection-request');
|
const prepareCollectionRequest = require('./prepare-collection-request');
|
||||||
@ -37,6 +37,8 @@ const {
|
|||||||
} = require('./oauth2-helper');
|
} = require('./oauth2-helper');
|
||||||
const Oauth2Store = require('../../store/oauth2');
|
const Oauth2Store = require('../../store/oauth2');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const { createFormData } = prepareRequest;
|
||||||
|
|
||||||
const safeStringifyJSON = (data) => {
|
const safeStringifyJSON = (data) => {
|
||||||
try {
|
try {
|
||||||
@ -423,6 +425,14 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
request.data = qs.stringify(request.data);
|
request.data = qs.stringify(request.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.headers['content-type'] === 'multipart/form-data') {
|
||||||
|
if (!(request.data instanceof FormData)) {
|
||||||
|
let form = createFormData(request.data, collectionPath);
|
||||||
|
request.data = form;
|
||||||
|
extend(request.headers, form.getHeaders());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return scriptResult;
|
return scriptResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -515,6 +525,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
const collectionRoot = get(collection, 'root', {});
|
const collectionRoot = get(collection, 'root', {});
|
||||||
const request = prepareRequest(item, collection);
|
const request = prepareRequest(item, collection);
|
||||||
|
request.__bruno__executionMode = 'standalone';
|
||||||
const envVars = getEnvVars(environment);
|
const envVars = getEnvVars(environment);
|
||||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||||
const brunoConfig = getBrunoConfig(collectionUid);
|
const brunoConfig = getBrunoConfig(collectionUid);
|
||||||
@ -707,6 +718,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
const collectionRoot = get(collection, 'root', {});
|
const collectionRoot = get(collection, 'root', {});
|
||||||
const _request = collectionRoot?.request;
|
const _request = collectionRoot?.request;
|
||||||
const request = prepareCollectionRequest(_request, collectionRoot, collectionPath);
|
const request = prepareCollectionRequest(_request, collectionRoot, collectionPath);
|
||||||
|
request.__bruno__executionMode = 'standalone';
|
||||||
const envVars = getEnvVars(environment);
|
const envVars = getEnvVars(environment);
|
||||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||||
const brunoConfig = getBrunoConfig(collectionUid);
|
const brunoConfig = getBrunoConfig(collectionUid);
|
||||||
@ -950,6 +962,8 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const request = prepareRequest(item, collection);
|
const request = prepareRequest(item, collection);
|
||||||
|
request.__bruno__executionMode = 'runner';
|
||||||
|
|
||||||
const requestUid = uuid();
|
const requestUid = uuid();
|
||||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const { interpolate } = require('@usebruno/common');
|
const { interpolate } = require('@usebruno/common');
|
||||||
const { each, forOwn, cloneDeep, find } = require('lodash');
|
const { each, forOwn, cloneDeep, find } = require('lodash');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
|
||||||
const getContentType = (headers = {}) => {
|
const getContentType = (headers = {}) => {
|
||||||
let contentType = '';
|
let contentType = '';
|
||||||
@ -76,6 +77,14 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
|||||||
request.data = JSON.parse(parsed);
|
request.data = JSON.parse(parsed);
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
|
} else if (contentType === 'multipart/form-data') {
|
||||||
|
if (typeof request.data === 'object' && !(request.data instanceof FormData)) {
|
||||||
|
try {
|
||||||
|
let parsed = JSON.stringify(request.data);
|
||||||
|
parsed = _interpolate(parsed);
|
||||||
|
request.data = JSON.parse(parsed);
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
request.data = _interpolate(request.data);
|
request.data = _interpolate(request.data);
|
||||||
}
|
}
|
||||||
@ -111,7 +120,8 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
|||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
request.url = url.origin + urlPathnameInterpolatedWithPathParams + url.search;
|
const trailingSlash = url.pathname.endsWith('/') ? '/' : '';
|
||||||
|
request.url = url.origin + urlPathnameInterpolatedWithPathParams + trailingSlash + url.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.proxy) {
|
if (request.proxy) {
|
||||||
@ -206,6 +216,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
|||||||
request.digestConfig.password = _interpolate(request.digestConfig.password) || '';
|
request.digestConfig.password = _interpolate(request.digestConfig.password) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interpolate vars for wsse auth
|
||||||
|
if (request.wsse) {
|
||||||
|
request.wsse.username = _interpolate(request.wsse.username) || '';
|
||||||
|
request.wsse.password = _interpolate(request.wsse.password) || '';
|
||||||
|
}
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { get, each, filter, extend, compact } = require('lodash');
|
const { get, each, filter, compact, forOwn } = require('lodash');
|
||||||
const decomment = require('decomment');
|
const decomment = require('decomment');
|
||||||
const FormData = require('form-data');
|
const FormData = require('form-data');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const crypto = require('node:crypto');
|
||||||
const { getTreePathFromCollectionToItem } = require('../../utils/collection');
|
const { getTreePathFromCollectionToItem } = require('../../utils/collection');
|
||||||
const { buildFormUrlEncodedPayload } = require('../../utils/common');
|
const { buildFormUrlEncodedPayload } = require('../../utils/common');
|
||||||
|
|
||||||
@ -165,27 +166,26 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseFormData = (datas, collectionPath) => {
|
const createFormData = (datas, collectionPath) => {
|
||||||
// make axios work in node using form data
|
// make axios work in node using form data
|
||||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
datas.forEach((item) => {
|
forOwn(datas, (value, key) => {
|
||||||
const value = item.value;
|
if (typeof value == 'string') {
|
||||||
const name = item.name;
|
form.append(key, value);
|
||||||
if (item.type === 'file') {
|
return;
|
||||||
const filePaths = value || [];
|
|
||||||
filePaths.forEach((filePath) => {
|
|
||||||
let trimmedFilePath = filePath.trim();
|
|
||||||
|
|
||||||
if (!path.isAbsolute(trimmedFilePath)) {
|
|
||||||
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.append(name, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filePaths = value || [];
|
||||||
|
filePaths?.forEach?.((filePath) => {
|
||||||
|
let trimmedFilePath = filePath.trim();
|
||||||
|
|
||||||
|
if (!path.isAbsolute(trimmedFilePath)) {
|
||||||
|
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return form;
|
return form;
|
||||||
};
|
};
|
||||||
@ -219,6 +219,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
|||||||
password: get(collectionAuth, 'digest.password')
|
password: get(collectionAuth, 'digest.password')
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case 'wsse':
|
||||||
|
const username = get(request, 'auth.wsse.username', '');
|
||||||
|
const password = get(request, 'auth.wsse.password', '');
|
||||||
|
|
||||||
|
const ts = new Date().toISOString();
|
||||||
|
const nonce = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
|
// Create the password digest using SHA-256
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.update(nonce + ts + password);
|
||||||
|
const digest = hash.digest('base64');
|
||||||
|
|
||||||
|
// Construct the WSSE header
|
||||||
|
axiosRequest.headers[
|
||||||
|
'X-WSSE'
|
||||||
|
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
|
||||||
|
break;
|
||||||
case 'apikey':
|
case 'apikey':
|
||||||
const apiKeyAuth = get(collectionAuth, 'apikey');
|
const apiKeyAuth = get(collectionAuth, 'apikey');
|
||||||
if (apiKeyAuth.placement === 'header') {
|
if (apiKeyAuth.placement === 'header') {
|
||||||
@ -296,6 +313,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'wsse':
|
||||||
|
const username = get(request, 'auth.wsse.username', '');
|
||||||
|
const password = get(request, 'auth.wsse.password', '');
|
||||||
|
|
||||||
|
const ts = new Date().toISOString();
|
||||||
|
const nonce = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
|
// Create the password digest using SHA-256
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.update(nonce + ts + password);
|
||||||
|
const digest = hash.digest('base64');
|
||||||
|
|
||||||
|
// Construct the WSSE header
|
||||||
|
axiosRequest.headers[
|
||||||
|
'X-WSSE'
|
||||||
|
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
|
||||||
|
break;
|
||||||
case 'apikey':
|
case 'apikey':
|
||||||
const apiKeyAuth = get(request, 'auth.apikey');
|
const apiKeyAuth = get(request, 'auth.apikey');
|
||||||
if (apiKeyAuth.placement === 'header') {
|
if (apiKeyAuth.placement === 'header') {
|
||||||
@ -400,10 +434,11 @@ const prepareRequest = (item, collection) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (request.body.mode === 'multipartForm') {
|
if (request.body.mode === 'multipartForm') {
|
||||||
|
axiosRequest.headers['content-type'] = 'multipart/form-data';
|
||||||
|
const params = {};
|
||||||
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
|
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
|
||||||
const form = parseFormData(enabledParams, collectionPath);
|
each(enabledParams, (p) => (params[p.name] = p.value));
|
||||||
extend(axiosRequest.headers, form.getHeaders());
|
axiosRequest.data = params;
|
||||||
axiosRequest.data = form;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.body.mode === 'graphql') {
|
if (request.body.mode === 'graphql') {
|
||||||
@ -433,3 +468,4 @@ const prepareRequest = (item, collection) => {
|
|||||||
|
|
||||||
module.exports = prepareRequest;
|
module.exports = prepareRequest;
|
||||||
module.exports.setAuthHeaders = setAuthHeaders;
|
module.exports.setAuthHeaders = setAuthHeaders;
|
||||||
|
module.exports.createFormData = createFormData;
|
||||||
|
@ -3,6 +3,7 @@ const fs = require('fs-extra');
|
|||||||
const fsPromises = require('fs/promises');
|
const fsPromises = require('fs/promises');
|
||||||
const { dialog } = require('electron');
|
const { dialog } = require('electron');
|
||||||
const isValidPathname = require('is-valid-path');
|
const isValidPathname = require('is-valid-path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
const exists = async (p) => {
|
const exists = async (p) => {
|
||||||
try {
|
try {
|
||||||
@ -155,12 +156,34 @@ const searchForBruFiles = (dir) => {
|
|||||||
return searchForFiles(dir, '.bru');
|
return searchForFiles(dir, '.bru');
|
||||||
};
|
};
|
||||||
|
|
||||||
// const isW
|
|
||||||
|
|
||||||
const sanitizeDirectoryName = (name) => {
|
const sanitizeDirectoryName = (name) => {
|
||||||
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
|
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const safeToRename = (oldPath, newPath) => {
|
||||||
|
try {
|
||||||
|
// If the new path doesn't exist, it's safe to rename
|
||||||
|
if (!fs.existsSync(newPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldStat = fs.statSync(oldPath);
|
||||||
|
const newStat = fs.statSync(newPath);
|
||||||
|
|
||||||
|
if (os.platform() === 'win32') {
|
||||||
|
// Windows-specific comparison:
|
||||||
|
// Check if both files have the same birth time, size (Since, Win FAT-32 doesn't use inodes)
|
||||||
|
|
||||||
|
return oldStat.birthtimeMs === newStat.birthtimeMs && oldStat.size === newStat.size;
|
||||||
|
}
|
||||||
|
// Unix/Linux/MacOS: Check inode to see if they are the same file
|
||||||
|
return oldStat.ino === newStat.ino;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error checking file rename safety for ${oldPath} and ${newPath}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isValidPathname,
|
isValidPathname,
|
||||||
exists,
|
exists,
|
||||||
@ -180,5 +203,6 @@ module.exports = {
|
|||||||
chooseFileToSave,
|
chooseFileToSave,
|
||||||
searchForFiles,
|
searchForFiles,
|
||||||
searchForBruFiles,
|
searchForBruFiles,
|
||||||
sanitizeDirectoryName
|
sanitizeDirectoryName,
|
||||||
|
safeToRename
|
||||||
};
|
};
|
||||||
|
@ -43,7 +43,6 @@ class BrunoRequest {
|
|||||||
getMethod() {
|
getMethod() {
|
||||||
return this.req.method;
|
return this.req.method;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthMode() {
|
getAuthMode() {
|
||||||
if (this.req?.oauth2) {
|
if (this.req?.oauth2) {
|
||||||
return 'oauth2';
|
return 'oauth2';
|
||||||
@ -55,6 +54,8 @@ class BrunoRequest {
|
|||||||
return 'awsv4';
|
return 'awsv4';
|
||||||
} else if (this.req?.digestConfig) {
|
} else if (this.req?.digestConfig) {
|
||||||
return 'digest';
|
return 'digest';
|
||||||
|
} else if (this.headers?.['X-WSSE'] || this.req?.auth?.username) {
|
||||||
|
return 'wsse';
|
||||||
} else {
|
} else {
|
||||||
return 'none';
|
return 'none';
|
||||||
}
|
}
|
||||||
@ -172,6 +173,10 @@ class BrunoRequest {
|
|||||||
disableParsingResponseJson() {
|
disableParsingResponseJson() {
|
||||||
this.req.__brunoDisableParsingResponseJson = true;
|
this.req.__brunoDisableParsingResponseJson = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExecutionMode() {
|
||||||
|
return this.req.__bruno__executionMode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = BrunoRequest;
|
module.exports = BrunoRequest;
|
||||||
|
@ -111,6 +111,12 @@ const addBrunoRequestShimToContext = (vm, req) => {
|
|||||||
vm.setProp(reqObject, 'disableParsingResponseJson', disableParsingResponseJson);
|
vm.setProp(reqObject, 'disableParsingResponseJson', disableParsingResponseJson);
|
||||||
disableParsingResponseJson.dispose();
|
disableParsingResponseJson.dispose();
|
||||||
|
|
||||||
|
let getExecutionMode = vm.newFunction('getExecutionMode', function () {
|
||||||
|
return marshallToVm(req.getExecutionMode(), vm);
|
||||||
|
});
|
||||||
|
vm.setProp(reqObject, 'getExecutionMode', getExecutionMode);
|
||||||
|
getExecutionMode.dispose();
|
||||||
|
|
||||||
vm.setProp(vm.global, 'req', reqObject);
|
vm.setProp(vm.global, 'req', reqObject);
|
||||||
reqObject.dispose();
|
reqObject.dispose();
|
||||||
};
|
};
|
||||||
|
@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils');
|
|||||||
*/
|
*/
|
||||||
const grammar = ohm.grammar(`Bru {
|
const grammar = ohm.grammar(`Bru {
|
||||||
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
|
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
|
||||||
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authapikey
|
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authwsse | authapikey
|
||||||
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
|
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
|
||||||
bodyforms = bodyformurlencoded | bodymultipart
|
bodyforms = bodyformurlencoded | bodymultipart
|
||||||
params = paramspath | paramsquery
|
params = paramspath | paramsquery
|
||||||
@ -88,6 +88,7 @@ const grammar = ohm.grammar(`Bru {
|
|||||||
authbearer = "auth:bearer" dictionary
|
authbearer = "auth:bearer" dictionary
|
||||||
authdigest = "auth:digest" dictionary
|
authdigest = "auth:digest" dictionary
|
||||||
authOAuth2 = "auth:oauth2" dictionary
|
authOAuth2 = "auth:oauth2" dictionary
|
||||||
|
authwsse = "auth:wsse" dictionary
|
||||||
authapikey = "auth:apikey" dictionary
|
authapikey = "auth:apikey" dictionary
|
||||||
|
|
||||||
body = "body" st* "{" nl* textblock tagend
|
body = "body" st* "{" nl* textblock tagend
|
||||||
@ -484,6 +485,23 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
authwsse(_1, dictionary) {
|
||||||
|
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||||
|
|
||||||
|
const userKey = _.find(auth, { name: 'username' });
|
||||||
|
const secretKey = _.find(auth, { name: 'password' });
|
||||||
|
const username = userKey ? userKey.value : '';
|
||||||
|
const password = secretKey ? secretKey.value : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
auth: {
|
||||||
|
wsse: {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
authapikey(_1, dictionary) {
|
authapikey(_1, dictionary) {
|
||||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ const { outdentString } = require('../../v1/src/utils');
|
|||||||
|
|
||||||
const grammar = ohm.grammar(`Bru {
|
const grammar = ohm.grammar(`Bru {
|
||||||
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
|
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
|
||||||
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authapikey
|
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authwsse | authapikey
|
||||||
|
|
||||||
nl = "\\r"? "\\n"
|
nl = "\\r"? "\\n"
|
||||||
st = " " | "\\t"
|
st = " " | "\\t"
|
||||||
@ -43,6 +43,7 @@ const grammar = ohm.grammar(`Bru {
|
|||||||
authbearer = "auth:bearer" dictionary
|
authbearer = "auth:bearer" dictionary
|
||||||
authdigest = "auth:digest" dictionary
|
authdigest = "auth:digest" dictionary
|
||||||
authOAuth2 = "auth:oauth2" dictionary
|
authOAuth2 = "auth:oauth2" dictionary
|
||||||
|
authwsse = "auth:wsse" dictionary
|
||||||
authapikey = "auth:apikey" dictionary
|
authapikey = "auth:apikey" dictionary
|
||||||
|
|
||||||
script = scriptreq | scriptres
|
script = scriptreq | scriptres
|
||||||
@ -294,6 +295,21 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
authwsse(_1, dictionary) {
|
||||||
|
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||||
|
const userKey = _.find(auth, { name: 'username' });
|
||||||
|
const secretKey = _.find(auth, { name: 'password' });
|
||||||
|
const username = userKey ? userKey.value : '';
|
||||||
|
const password = secretKey ? secretKey.value : '';
|
||||||
|
return {
|
||||||
|
auth: {
|
||||||
|
wsse: {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
authapikey(_1, dictionary) {
|
authapikey(_1, dictionary) {
|
||||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||||
|
|
||||||
|
@ -136,6 +136,15 @@ ${indentString(`username: ${auth?.basic?.username || ''}`)}
|
|||||||
${indentString(`password: ${auth?.basic?.password || ''}`)}
|
${indentString(`password: ${auth?.basic?.password || ''}`)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth && auth.wsse) {
|
||||||
|
bru += `auth:wsse {
|
||||||
|
${indentString(`username: ${auth?.wsse?.username || ''}`)}
|
||||||
|
${indentString(`password: ${auth?.wsse?.password || ''}`)}
|
||||||
|
}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,6 +94,15 @@ ${indentString(`username: ${auth.basic.username}`)}
|
|||||||
${indentString(`password: ${auth.basic.password}`)}
|
${indentString(`password: ${auth.basic.password}`)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth && auth.wsse) {
|
||||||
|
bru += `auth:wsse {
|
||||||
|
${indentString(`username: ${auth.wsse.username}`)}
|
||||||
|
${indentString(`password: ${auth.wsse.password}`)}
|
||||||
|
}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,11 @@ auth:basic {
|
|||||||
password: secret
|
password: secret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth:wsse {
|
||||||
|
username: john
|
||||||
|
password: secret
|
||||||
|
}
|
||||||
|
|
||||||
auth:bearer {
|
auth:bearer {
|
||||||
token: 123
|
token: 123
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,10 @@
|
|||||||
"digest": {
|
"digest": {
|
||||||
"username": "john",
|
"username": "john",
|
||||||
"password": "secret"
|
"password": "secret"
|
||||||
|
},
|
||||||
|
"wsse": {
|
||||||
|
"username": "john",
|
||||||
|
"password": "secret"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vars": {
|
"vars": {
|
||||||
|
@ -40,6 +40,11 @@ auth:basic {
|
|||||||
password: secret
|
password: secret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth:wsse {
|
||||||
|
username: john
|
||||||
|
password: secret
|
||||||
|
}
|
||||||
|
|
||||||
auth:bearer {
|
auth:bearer {
|
||||||
token: 123
|
token: 123
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,10 @@
|
|||||||
"scope": "read write",
|
"scope": "read write",
|
||||||
"state": "807061d5f0be",
|
"state": "807061d5f0be",
|
||||||
"pkce": false
|
"pkce": false
|
||||||
|
},
|
||||||
|
"wsse": {
|
||||||
|
"username": "john",
|
||||||
|
"password": "secret"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
|
@ -106,6 +106,13 @@ const authBasicSchema = Yup.object({
|
|||||||
.noUnknown(true)
|
.noUnknown(true)
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const authWsseSchema = Yup.object({
|
||||||
|
username: Yup.string().nullable(),
|
||||||
|
password: Yup.string().nullable()
|
||||||
|
})
|
||||||
|
.noUnknown(true)
|
||||||
|
.strict();
|
||||||
|
|
||||||
const authBearerSchema = Yup.object({
|
const authBearerSchema = Yup.object({
|
||||||
token: Yup.string().nullable()
|
token: Yup.string().nullable()
|
||||||
})
|
})
|
||||||
@ -119,6 +126,14 @@ const authDigestSchema = Yup.object({
|
|||||||
.noUnknown(true)
|
.noUnknown(true)
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const authApiKeySchema = Yup.object({
|
||||||
|
key: Yup.string().nullable(),
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
placement: Yup.string().oneOf(['header', 'queryparams']).nullable()
|
||||||
|
})
|
||||||
|
.noUnknown(true)
|
||||||
|
.strict();
|
||||||
|
|
||||||
const oauth2Schema = Yup.object({
|
const oauth2Schema = Yup.object({
|
||||||
grantType: Yup.string()
|
grantType: Yup.string()
|
||||||
.oneOf(['client_credentials', 'password', 'authorization_code'])
|
.oneOf(['client_credentials', 'password', 'authorization_code'])
|
||||||
@ -177,21 +192,16 @@ const oauth2Schema = Yup.object({
|
|||||||
.noUnknown(true)
|
.noUnknown(true)
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
const authApiKeySchema = Yup.object({
|
|
||||||
key: Yup.string().nullable(),
|
|
||||||
value: Yup.string().nullable(),
|
|
||||||
placement: Yup.string().oneOf(['header', 'queryparams']).nullable()
|
|
||||||
});
|
|
||||||
|
|
||||||
const authSchema = Yup.object({
|
const authSchema = Yup.object({
|
||||||
mode: Yup.string()
|
mode: Yup.string()
|
||||||
.oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2', 'apikey'])
|
.oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2', 'wsse', 'apikey'])
|
||||||
.required('mode is required'),
|
.required('mode is required'),
|
||||||
awsv4: authAwsV4Schema.nullable(),
|
awsv4: authAwsV4Schema.nullable(),
|
||||||
basic: authBasicSchema.nullable(),
|
basic: authBasicSchema.nullable(),
|
||||||
bearer: authBearerSchema.nullable(),
|
bearer: authBearerSchema.nullable(),
|
||||||
digest: authDigestSchema.nullable(),
|
digest: authDigestSchema.nullable(),
|
||||||
oauth2: oauth2Schema.nullable(),
|
oauth2: oauth2Schema.nullable(),
|
||||||
|
wsse: authWsseSchema.nullable(),
|
||||||
apikey: authApiKeySchema.nullable()
|
apikey: authApiKeySchema.nullable()
|
||||||
})
|
})
|
||||||
.noUnknown(true)
|
.noUnknown(true)
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
"bypassProxy": ""
|
"bypassProxy": ""
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"moduleWhitelist": ["crypto", "buffer"],
|
"moduleWhitelist": ["crypto", "buffer", "form-data"],
|
||||||
"filesystemAccess": {
|
"filesystemAccess": {
|
||||||
"allow": true
|
"allow": true
|
||||||
}
|
}
|
||||||
|
BIN
packages/bruno-tests/collection/bruno.png
Normal file
BIN
packages/bruno-tests/collection/bruno.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 795 B |
@ -0,0 +1,23 @@
|
|||||||
|
meta {
|
||||||
|
name: echo form-url-encoded
|
||||||
|
type: http
|
||||||
|
seq: 9
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{echo-host}}
|
||||||
|
body: formUrlEncoded
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:form-urlencoded {
|
||||||
|
form-data-key: {{form-data-key}}
|
||||||
|
}
|
||||||
|
|
||||||
|
script:pre-request {
|
||||||
|
bru.setVar('form-data-key', 'form-data-value');
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.body: eq form-data-key=form-data-value
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
meta {
|
||||||
|
name: echo multipart via scripting
|
||||||
|
type: http
|
||||||
|
seq: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{echo-host}}
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.body: contains form-data-value
|
||||||
|
}
|
||||||
|
|
||||||
|
script:pre-request {
|
||||||
|
const FormData = require("form-data");
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('form-data-key', 'form-data-value');
|
||||||
|
req.setBody(form);
|
||||||
|
}
|
24
packages/bruno-tests/collection/echo/echo multipart.bru
Normal file
24
packages/bruno-tests/collection/echo/echo multipart.bru
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
meta {
|
||||||
|
name: echo multipart
|
||||||
|
type: http
|
||||||
|
seq: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{echo-host}}
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
foo: {{form-data-key}}
|
||||||
|
file: @file(bruno.png)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.body: contains form-data-value
|
||||||
|
}
|
||||||
|
|
||||||
|
script:pre-request {
|
||||||
|
bru.setVar('form-data-key', 'form-data-value');
|
||||||
|
}
|
@ -7,4 +7,5 @@ vars {
|
|||||||
bark: {{process.env.PROC_ENV_VAR}}
|
bark: {{process.env.PROC_ENV_VAR}}
|
||||||
foo: bar
|
foo: bar
|
||||||
testSetEnvVar: bruno-29653
|
testSetEnvVar: bruno-29653
|
||||||
|
echo-host: https://echo.usebruno.com
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ const router = express.Router();
|
|||||||
|
|
||||||
const authBearer = require('./bearer');
|
const authBearer = require('./bearer');
|
||||||
const authBasic = require('./basic');
|
const authBasic = require('./basic');
|
||||||
|
const authWsse = require('./wsse');
|
||||||
const authCookie = require('./cookie');
|
const authCookie = require('./cookie');
|
||||||
const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials');
|
const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials');
|
||||||
const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode');
|
const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode');
|
||||||
@ -13,6 +14,7 @@ router.use('/oauth2/authorization_code', authOAuth2AuthorizationCode);
|
|||||||
router.use('/oauth2/client_credentials', authOAuth2ClientCredentials);
|
router.use('/oauth2/client_credentials', authOAuth2ClientCredentials);
|
||||||
router.use('/bearer', authBearer);
|
router.use('/bearer', authBearer);
|
||||||
router.use('/basic', authBasic);
|
router.use('/basic', authBasic);
|
||||||
|
router.use('/wsse', authWsse);
|
||||||
router.use('/cookie', authCookie);
|
router.use('/cookie', authCookie);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
70
packages/bruno-tests/src/auth/wsse.js
Normal file
70
packages/bruno-tests/src/auth/wsse.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
function sha256(data) {
|
||||||
|
return crypto.createHash('sha256').update(data).digest('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateWSSE(req, res, next) {
|
||||||
|
const wsseHeader = req.headers['x-wsse'];
|
||||||
|
if (!wsseHeader) {
|
||||||
|
return unauthorized(res, 'WSSE header is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = /UsernameToken Username="(.+?)", PasswordDigest="(.+?)", (?:Nonce|nonce)="(.+?)", Created="(.+?)"/;
|
||||||
|
const matches = wsseHeader.match(regex);
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
|
return unauthorized(res, 'Invalid WSSE header format');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [_, username, passwordDigest, nonce, created] = matches;
|
||||||
|
const expectedPassword = 'bruno'; // Ideally store in a config or env variable
|
||||||
|
const expectedDigest = sha256(nonce + created + expectedPassword);
|
||||||
|
|
||||||
|
if (passwordDigest !== expectedDigest) {
|
||||||
|
return unauthorized(res, 'Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to respond with an unauthorized SOAP fault
|
||||||
|
function unauthorized(res, message) {
|
||||||
|
const faultResponse = `
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://webservice/">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<soapenv:Fault>
|
||||||
|
<faultcode>soapenv:Client</faultcode>
|
||||||
|
<faultstring>${message}</faultstring>
|
||||||
|
</soapenv:Fault>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
`;
|
||||||
|
res.status(401).set('Content-Type', 'text/xml');
|
||||||
|
res.send(faultResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = {
|
||||||
|
success: `
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://webservice/">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<web:response>
|
||||||
|
<web:result>Success</web:result>
|
||||||
|
</web:response>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
router.post('/protected', validateWSSE, (req, res) => {
|
||||||
|
res.set('Content-Type', 'text/xml');
|
||||||
|
res.send(responses.success);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
Loading…
x
Reference in New Issue
Block a user