mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
cURL: Support all --data flags and fix parsing of key=value pairs (#5818)
* test(curl): data-urlencode support key=value pairs Updates tests to support key=value pairs in urlencode Issue: https://github.com/Kong/insomnia/issues/5696 * fix(curl): data-urlencode support key=value pairs Splits out the logic to handle the cases for the --data-urlencode flag from cURL. docs: https://curl.se/docs/manpage.html#--data-urlencode Issue: https://github.com/Kong/insomnia/issues/5696 * feat(cURL): support all data flags This refactors the current implementation for handling --data[suffix] flags from cURL supporting all possible formats outside of reading files into Insomnia. docs: https://curl.se/docs/manpage.html Issues: https://github.com/Kong/insomnia/issues/5696, https://github.com/Kong/insomnia/issues/5400 * test(cURL): support all data flags Combines a lot of tests and covers all cases provided in the cURL man pages. * style: lint
This commit is contained in:
parent
29acc92559
commit
bed21be8af
@ -1,58 +1,156 @@
|
||||
import { describe, expect, it } from '@jest/globals';
|
||||
import { quote } from 'shell-quote';
|
||||
|
||||
import { Parameter } from '../entities';
|
||||
import { convert } from './curl';
|
||||
|
||||
describe('curl', () => {
|
||||
const url = 'http://example.com';
|
||||
const method = '-X POST';
|
||||
const mimeType = 'application/x-www-form-urlencoded';
|
||||
const header = `-H 'Content-Type: ${mimeType}'`;
|
||||
|
||||
describe('encoding', () => {
|
||||
describe('cURL --data flags', () => {
|
||||
it.each([
|
||||
{ input: ' ', expected: ' ' },
|
||||
{ input: 'a=', expected: 'a' }, // using `a` before `=` to disambiguate shell parameters
|
||||
{ input: '<', expected: '<' },
|
||||
{ input: '>', expected: '>' },
|
||||
{ input: '[', expected: '[' },
|
||||
{ input: ']', expected: ']' },
|
||||
{ input: '{', expected: '{' },
|
||||
{ input: '}', expected: '}' },
|
||||
{ input: '|', expected: '|' },
|
||||
{ input: '^', expected: '^' },
|
||||
{ input: '%3d', expected: '=' },
|
||||
{ input: '"', expected: '"' },
|
||||
])('encodes %p correctly', ({ input, expected }: { input: string; expected: string }) => {
|
||||
const quoted = quote([input]);
|
||||
const rawData = `curl ${method} ${url} ${header} --data ${quoted} --data-urlencode ${quoted}`;
|
||||
// -d
|
||||
{ flag: '-d', inputs: ['key=value'], expected: [{ name: 'key', value: 'value' }] },
|
||||
{ flag: '-d', inputs: ['value'], expected: [{ name: '', value: 'value' }] },
|
||||
{ flag: '-d', inputs: ['@filename'], expected: [{ name: '', fileName: 'filename', type: 'file' }] },
|
||||
{
|
||||
flag: '-d',
|
||||
inputs: ['first=1', 'second=2', 'third'],
|
||||
expected: [{ name: 'first', value: '1' }, {
|
||||
name: 'second',
|
||||
value: '2',
|
||||
}, { name: '', value: 'third' }],
|
||||
},
|
||||
{
|
||||
flag: '-d',
|
||||
inputs: ['first=1&second=2'],
|
||||
expected: [{ name: 'first', value: '1' }, { name: 'second', value: '2' }],
|
||||
},
|
||||
{ flag: '-d', inputs: ['%3D'], expected: [{ name: '', value: '=' }] },
|
||||
{ flag: '--d', inputs: ['%3D=%3D'], expected: [{ name: '=', value: '=' }] },
|
||||
|
||||
// --data
|
||||
{ flag: '--data', inputs: ['key=value'], expected: [{ name: 'key', value: 'value' }] },
|
||||
{ flag: '--data', inputs: ['value'], expected: [{ name: '', value: 'value' }] },
|
||||
{ flag: '--data', inputs: ['@filename'], expected: [{ name: '', fileName: 'filename' }] },
|
||||
{
|
||||
flag: '--data',
|
||||
inputs: ['first=1', 'second=2', 'third'],
|
||||
expected: [{ name: 'first', value: '1' }, {
|
||||
name: 'second',
|
||||
value: '2',
|
||||
}, { name: '', value: 'third' }],
|
||||
},
|
||||
{
|
||||
flag: '--data',
|
||||
inputs: ['first=1&second=2'],
|
||||
expected: [{ name: 'first', value: '1' }, { name: 'second', value: '2' }],
|
||||
},
|
||||
{ flag: '--data', inputs: ['%3D'], expected: [{ name: '', value: '=' }] },
|
||||
{ flag: '--data', inputs: ['%3D=%3D'], expected: [{ name: '=', value: '=' }] },
|
||||
|
||||
// --data-ascii
|
||||
{ flag: '--data-ascii', inputs: ['key=value'], expected: [{ name: 'key', value: 'value' }] },
|
||||
{ flag: '--data-ascii', inputs: ['value'], expected: [{ name: '', value: 'value' }] },
|
||||
{ flag: '--data-ascii', inputs: ['@filename'], expected: [{ name: '', fileName: 'filename', type: 'file' }] },
|
||||
{
|
||||
flag: '--data-ascii',
|
||||
inputs: ['first=1', 'second=2', 'third'],
|
||||
expected: [{ name: 'first', value: '1' }, {
|
||||
name: 'second',
|
||||
value: '2',
|
||||
}, { name: '', value: 'third' }],
|
||||
},
|
||||
{
|
||||
flag: '--data-ascii',
|
||||
inputs: ['first=1&second=2'],
|
||||
expected: [{ name: 'first', value: '1' }, { name: 'second', value: '2' }],
|
||||
},
|
||||
|
||||
// --data-binary
|
||||
{ flag: '--data-binary', inputs: ['key=value'], expected: [{ name: 'key', value: 'value' }] },
|
||||
{ flag: '--data-binary', inputs: ['value'], expected: [{ name: '', value: 'value' }] },
|
||||
{ flag: '--data-binary', inputs: ['@filename'], expected: [{ name: '', fileName: 'filename', type: 'file' }] },
|
||||
{
|
||||
flag: '--data-binary',
|
||||
inputs: ['first=1', 'second=2', 'third'],
|
||||
expected: [{ name: 'first', value: '1' }, {
|
||||
name: 'second',
|
||||
value: '2',
|
||||
}, { name: '', value: 'third' }],
|
||||
},
|
||||
{
|
||||
flag: '--data-binary',
|
||||
inputs: ['first=1&second=2'],
|
||||
expected: [{ name: 'first', value: '1' }, { name: 'second', value: '2' }],
|
||||
},
|
||||
|
||||
// --data-raw
|
||||
{ flag: '--data-raw', inputs: ['@filename'], expected: [{ name: '', value: '@filename' }] },
|
||||
{ flag: '--data-raw', inputs: ['key=value'], expected: [{ name: 'key', value: 'value' }] },
|
||||
{
|
||||
flag: '--data-raw',
|
||||
inputs: ['first=1', 'second=2', 'third'],
|
||||
expected: [{ name: 'first', value: '1' }, {
|
||||
name: 'second',
|
||||
value: '2',
|
||||
}, { name: '', value: 'third' }],
|
||||
},
|
||||
{
|
||||
flag: '--data-raw',
|
||||
inputs: ['first=1&second=2'],
|
||||
expected: [{ name: 'first', value: '1' }, { name: 'second', value: '2' }],
|
||||
},
|
||||
|
||||
// --data-urlencode
|
||||
{ flag: '--data-urlencode', inputs: ['key=value'], expected: [{ name: 'key', value: 'value' }] },
|
||||
{
|
||||
flag: '--data-urlencode',
|
||||
inputs: ['key@filename'],
|
||||
expected: [{ name: 'key', fileName: 'filename', type: 'file' }],
|
||||
},
|
||||
{
|
||||
flag: '--data-urlencode',
|
||||
inputs: ['first=1', 'second=2', 'third'],
|
||||
expected: [{ name: 'first', value: '1' }, {
|
||||
name: 'second',
|
||||
value: '2',
|
||||
}, { name: '', value: 'third' }],
|
||||
},
|
||||
{
|
||||
flag: '--data-urlencode',
|
||||
inputs: ['first=1&second=2'],
|
||||
expected: [{ name: 'first', value: '1' }, { name: 'second', value: '2' }],
|
||||
},
|
||||
{ flag: '--data-urlencode', inputs: ['=value'], expected: [{ name: '', value: 'value' }] },
|
||||
|
||||
// --data-urlencode URI encoding
|
||||
{ flag: '--data-urlencode', inputs: ['a='], expected: [{ name: '', value: 'a=' }] },
|
||||
{ flag: '--data-urlencode', inputs: [' '], expected: [{ name: '', value: ' ' }] },
|
||||
{ flag: '--data-urlencode', inputs: ['<'], expected: [{ name: '', value: '<' }] },
|
||||
{ flag: '--data-urlencode', inputs: ['>'], expected: [{ name: '', value: '>' }] },
|
||||
{ flag: '--data-urlencode', inputs: ['?'], expected: [{ name: '', value: '?' }] },
|
||||
{ flag: '--data-urlencode', inputs: ['['], expected: [{ name: '', value: '[' }] },
|
||||
{ flag: '--data-urlencode', inputs: [']'], expected: [{ name: '', value: ']' }] },
|
||||
{ flag: '--data-urlencode', inputs: ['|'], expected: [{ name: '', value: '|' }] },
|
||||
{ flag: '--data-urlencode', inputs: ['^'], expected: [{ name: '', value: '^' }] },
|
||||
{ flag: '--data-urlencode', inputs: ['"'], expected: [{ name: '', value: '"' }] },
|
||||
{ flag: '--data-urlencode', inputs: ['='], expected: [{ name: '', value: '=' }] },
|
||||
{ flag: '--data-urlencode', inputs: ['%3D'], expected: [{ name: '', value: '%3D' }] },
|
||||
])('handles %p correctly', async ({
|
||||
flag,
|
||||
inputs,
|
||||
expected,
|
||||
}: { flag: string; inputs: string[]; expected: Parameter[] }) => {
|
||||
const flaggedInputs = inputs.map(input => `${flag} ${quote([input])}`).join(' ');
|
||||
const rawData = `curl -X POST https://example.com
|
||||
-H 'Content-Type: application/x-www-form-urlencoded'
|
||||
${flaggedInputs}
|
||||
`;
|
||||
|
||||
expect(convert(rawData)).toMatchObject([{
|
||||
body: {
|
||||
params: [
|
||||
{ name: expected, value: '' },
|
||||
{ name: input, value: '' },
|
||||
],
|
||||
params: expected,
|
||||
},
|
||||
}]);
|
||||
});
|
||||
|
||||
it('handles & correctly', () => {
|
||||
const rawData = `curl ${method} ${url} ${header} --data a=1\\&b=2 --data-urlencode c=3\\&d=4`;
|
||||
expect(convert(rawData)).toMatchObject([{
|
||||
body: {
|
||||
params: [
|
||||
{ name: 'a', value: '1' },
|
||||
{ name: 'b', value: '2' },
|
||||
{ name: 'c=3&d=4', value: '' },
|
||||
],
|
||||
},
|
||||
}]);
|
||||
});
|
||||
|
||||
it('throws on invalid url encoding', () => {
|
||||
const rawData = `curl ${method} ${url} ${header} --data %`;
|
||||
expect(() => convert(rawData)).toThrow('URI malformed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -158,26 +158,7 @@ const importCommand = (parseEntries: ParseEntry[]): ImportRequest => {
|
||||
}
|
||||
|
||||
/// /////// Body (Text or Blob) //////////
|
||||
let textBodyParams: Pair[] = [];
|
||||
const paramNames = [
|
||||
'd',
|
||||
'data',
|
||||
'data-raw',
|
||||
'data-urlencode',
|
||||
'data-binary',
|
||||
'data-ascii',
|
||||
];
|
||||
|
||||
for (const paramName of paramNames) {
|
||||
const pair = pairsByName[paramName];
|
||||
|
||||
if (pair && pair.length) {
|
||||
textBodyParams = textBodyParams.concat(paramName === 'data-urlencode' ? pair.map(item => encodeURIComponent(item)) : pair);
|
||||
}
|
||||
}
|
||||
|
||||
// join params to make body
|
||||
const textBody = textBodyParams.join('&');
|
||||
const dataParameters = pairsToDataParameters(pairsByName);
|
||||
const contentTypeHeader = headers.find(
|
||||
header => header.name.toLowerCase() === 'content-type',
|
||||
);
|
||||
@ -210,25 +191,18 @@ const importCommand = (parseEntries: ParseEntry[]): ImportRequest => {
|
||||
const body: PostData = mimeType ? { mimeType } : {};
|
||||
const bodyAsGET = getPairValue(pairsByName, false, ['G', 'get']);
|
||||
|
||||
if (textBody && bodyAsGET) {
|
||||
const bodyParams = textBody.split('&').map(v => {
|
||||
const [name, value] = v.split('=');
|
||||
if (dataParameters.length !== 0 && bodyAsGET) {
|
||||
parameters.push(...dataParameters);
|
||||
} else if (dataParameters && mimeType === 'application/x-www-form-urlencoded') {
|
||||
body.params = dataParameters.map(parameter => {
|
||||
return {
|
||||
name: name || '',
|
||||
value: value || '',
|
||||
...parameter,
|
||||
name: decodeURIComponent(parameter.name || ''),
|
||||
value: decodeURIComponent(parameter.value || ''),
|
||||
};
|
||||
});
|
||||
parameters.push(...bodyParams);
|
||||
} else if (textBody && mimeType === 'application/x-www-form-urlencoded') {
|
||||
body.params = textBody.split('&').map(v => {
|
||||
const [name, value] = v.split('=');
|
||||
return {
|
||||
name: decodeURIComponent(name || ''),
|
||||
value: decodeURIComponent(value || ''),
|
||||
};
|
||||
});
|
||||
} else if (textBody) {
|
||||
body.text = textBody;
|
||||
} else if (dataParameters.length !== 0) {
|
||||
body.text = dataParameters.map(parameter => `${parameter.name}${parameter.value}`).join('&');
|
||||
body.mimeType = mimeType || '';
|
||||
} else if (formDataParams.length) {
|
||||
body.params = formDataParams;
|
||||
@ -260,6 +234,110 @@ const importCommand = (parseEntries: ParseEntry[]): ImportRequest => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* cURL supported -d, and --date[suffix] flags.
|
||||
*/
|
||||
const dataFlags = [
|
||||
/**
|
||||
* https://curl.se/docs/manpage.html#-d
|
||||
*/
|
||||
'd',
|
||||
'data',
|
||||
|
||||
/**
|
||||
* https://curl.se/docs/manpage.html#--data-raw
|
||||
*/
|
||||
'data-raw',
|
||||
|
||||
/**
|
||||
* https://curl.se/docs/manpage.html#--data-urlencode
|
||||
*/
|
||||
'data-urlencode',
|
||||
|
||||
/**
|
||||
* https://curl.se/docs/manpage.html#--data-binary
|
||||
*/
|
||||
'data-binary',
|
||||
|
||||
/**
|
||||
* https://curl.se/docs/manpage.html#--data-ascii
|
||||
*/
|
||||
'data-ascii',
|
||||
];
|
||||
|
||||
/**
|
||||
* Parses pairs supporting only flags dictated by {@link dataFlags}
|
||||
*
|
||||
* @param keyedPairs pairs with cURL flags as keys.
|
||||
*/
|
||||
const pairsToDataParameters = (keyedPairs: PairsByName): Parameter[] => {
|
||||
let dataParameters: Parameter[] = [];
|
||||
|
||||
for (const flagName of dataFlags) {
|
||||
const pairs = keyedPairs[flagName];
|
||||
|
||||
if (!pairs || pairs.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (flagName) {
|
||||
case 'd':
|
||||
case 'data':
|
||||
case 'data-ascii':
|
||||
case 'data-binary':
|
||||
dataParameters = dataParameters.concat(pairs.flatMap(pair => pairToParameters(pair, true)));
|
||||
break;
|
||||
case 'data-raw':
|
||||
dataParameters = dataParameters.concat(pairs.flatMap(pair => pairToParameters(pair)));
|
||||
break;
|
||||
case 'data-urlencode':
|
||||
dataParameters = dataParameters.concat(pairs.flatMap(pair => pairToParameters(pair, true))
|
||||
.map(parameter => {
|
||||
if (parameter.type === 'file') {
|
||||
return parameter;
|
||||
}
|
||||
|
||||
return {
|
||||
...parameter,
|
||||
value: encodeURIComponent(parameter.value ?? ''),
|
||||
};
|
||||
}));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unhandled data flag ${flagName}`);
|
||||
}
|
||||
}
|
||||
|
||||
return dataParameters;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts pairs (that could include multiple via `&`) into {@link Parameter}s. This
|
||||
* method supports both `@filename` and `name@filename`.
|
||||
*
|
||||
* @param pair command line value
|
||||
* @param allowFiles whether to allow the `@` to support include files
|
||||
*/
|
||||
const pairToParameters = (pair: Pair, allowFiles = false): Parameter[] => {
|
||||
if (typeof pair === 'boolean') {
|
||||
return [{ name: '', value: pair.toString() }];
|
||||
}
|
||||
|
||||
return pair.split('&').map(pair => {
|
||||
if (pair.includes('@') && allowFiles) {
|
||||
const [name, fileName] = pair.split('@');
|
||||
return { name, fileName, type: 'file' };
|
||||
}
|
||||
|
||||
const [name, value] = pair.split('=');
|
||||
if (!value || !pair.includes('=')) {
|
||||
return { name: '', value: pair };
|
||||
}
|
||||
|
||||
return { name, value };
|
||||
});
|
||||
};
|
||||
|
||||
const getPairValue = <T extends string | boolean>(
|
||||
parisByName: PairsByName,
|
||||
defaultValue: T,
|
||||
|
Loading…
Reference in New Issue
Block a user