mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 04:35:40 +00:00
fix: cURL auth import for digest and ntlm (#6292)
This commit is contained in:
@@ -1,8 +1,34 @@
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { getAuthHeaders } from 'utils/codegenerator/auth';
|
||||
import { getAllVariables, getTreePathFromCollectionToItem, mergeHeaders } from 'utils/collections/index';
|
||||
import { resolveInheritedAuth } from 'utils/auth';
|
||||
import { get } from 'lodash';
|
||||
import { interpolateAuth, interpolateHeaders, interpolateBody, interpolateParams } from './interpolation';
|
||||
|
||||
const addCurlAuthFlags = (curlCommand, auth) => {
|
||||
if (!auth || !curlCommand) return curlCommand;
|
||||
|
||||
const authMode = auth.mode;
|
||||
|
||||
if (authMode === 'digest' || authMode === 'ntlm') {
|
||||
const username = get(auth, `${authMode}.username`, '');
|
||||
const password = get(auth, `${authMode}.password`, '');
|
||||
const credentials = password ? `${username}:${password}` : username;
|
||||
const authFlag = authMode === 'digest' ? '--digest' : '--ntlm';
|
||||
// Escape single quotes for shell safety: ' becomes '\''
|
||||
const escapedCredentials = credentials.replace(/'/g, `'\\''`);
|
||||
|
||||
const curlMatch = curlCommand.match(/^(curl(?:\.exe)?)/i);
|
||||
if (curlMatch) {
|
||||
const curlCmd = curlMatch[1];
|
||||
const restOfCommand = curlCommand.slice(curlCmd.length);
|
||||
return `${curlCmd} ${authFlag} --user '${escapedCredentials}'${restOfCommand}`;
|
||||
}
|
||||
}
|
||||
|
||||
return curlCommand;
|
||||
};
|
||||
|
||||
const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => {
|
||||
try {
|
||||
// Get HTTPSnippet dynamically so mocks can be applied in tests
|
||||
@@ -11,6 +37,12 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
const variables = getAllVariables(collection, item);
|
||||
const request = item.request;
|
||||
|
||||
let effectiveAuth = request.auth;
|
||||
if (request.auth?.mode === 'inherit') {
|
||||
const resolvedRequest = resolveInheritedAuth(item, collection);
|
||||
effectiveAuth = resolvedRequest.auth;
|
||||
}
|
||||
|
||||
// Get the request tree path and merge headers
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
let headers = mergeHeaders(collection, request, requestTreePath);
|
||||
@@ -40,7 +72,12 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
|
||||
// Generate snippet using HTTPSnippet
|
||||
const snippet = new HTTPSnippet(harRequest);
|
||||
const result = snippet.convert(language.target, language.client);
|
||||
let result = snippet.convert(language.target, language.client);
|
||||
|
||||
// For curl target, add special auth flags for digest/ntlm
|
||||
if (language.target === 'shell' && language.client === 'curl') {
|
||||
result = addCurlAuthFlags(result, effectiveAuth);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
|
||||
@@ -827,3 +827,82 @@ describe('generateSnippet with OAuth2 authentication', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSnippet – digest and NTLM auth curl export', () => {
|
||||
const language = { target: 'shell', client: 'curl' };
|
||||
|
||||
const baseCollection = {
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
auth: { mode: 'none' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it('should add --digest flag and --user for digest auth', () => {
|
||||
const item = {
|
||||
uid: 'digest-req',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/api',
|
||||
headers: [],
|
||||
body: { mode: 'none' },
|
||||
auth: {
|
||||
mode: 'digest',
|
||||
digest: {
|
||||
username: 'myuser',
|
||||
password: 'mypass'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
|
||||
expect(result).toMatch(/^curl --digest --user 'myuser:mypass'/);
|
||||
});
|
||||
|
||||
it('should add --ntlm flag and --user for NTLM auth', () => {
|
||||
const item = {
|
||||
uid: 'ntlm-req',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/api',
|
||||
headers: [],
|
||||
body: { mode: 'none' },
|
||||
auth: {
|
||||
mode: 'ntlm',
|
||||
ntlm: {
|
||||
username: 'myuser',
|
||||
password: 'mypass'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
|
||||
expect(result).toMatch(/^curl --ntlm --user 'myuser:mypass'/);
|
||||
});
|
||||
|
||||
it('should handle digest auth with username only (no password)', () => {
|
||||
const item = {
|
||||
uid: 'digest-no-pass',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/api',
|
||||
headers: [],
|
||||
body: { mode: 'none' },
|
||||
auth: {
|
||||
mode: 'digest',
|
||||
digest: {
|
||||
username: 'myuser',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
|
||||
expect(result).toMatch(/^curl --digest --user 'myuser'/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,7 +184,8 @@ const curlToJson = (curlCommand) => {
|
||||
}
|
||||
|
||||
if (request.auth) {
|
||||
if (request.auth.mode === 'basic') {
|
||||
const authMode = request.auth.mode;
|
||||
if (authMode === 'basic') {
|
||||
requestJson.auth = {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
@@ -192,6 +193,22 @@ const curlToJson = (curlCommand) => {
|
||||
password: repr(request.auth.basic?.password)
|
||||
}
|
||||
};
|
||||
} else if (authMode === 'digest') {
|
||||
requestJson.auth = {
|
||||
mode: 'digest',
|
||||
digest: {
|
||||
username: repr(request.auth.digest?.username),
|
||||
password: repr(request.auth.digest?.password)
|
||||
}
|
||||
};
|
||||
} else if (authMode === 'ntlm') {
|
||||
requestJson.auth = {
|
||||
mode: 'ntlm',
|
||||
ntlm: {
|
||||
username: repr(request.auth.ntlm?.username),
|
||||
password: repr(request.auth.ntlm?.password)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ const FLAG_CATEGORIES = {
|
||||
'head': ['-I', '--head'],
|
||||
'compressed': ['--compressed'],
|
||||
'insecure': ['-k', '--insecure'],
|
||||
'digest': ['--digest'],
|
||||
'ntlm': ['--ntlm'],
|
||||
/**
|
||||
* Query flags: mark data for conversion to query parameters.
|
||||
* While this is an immediate action flag, the actual conversion to a query string occurs later during post-build request processing.
|
||||
@@ -149,6 +151,14 @@ const handleFlagCategory = (category, arg, request) => {
|
||||
request.insecure = true;
|
||||
return null;
|
||||
|
||||
case 'digest':
|
||||
request.isDigestAuth = true;
|
||||
return null;
|
||||
|
||||
case 'ntlm':
|
||||
request.isNtlmAuth = true;
|
||||
return null;
|
||||
|
||||
case 'query':
|
||||
// set temporary property isQuery to true to indicate that the data should be converted to query string
|
||||
// this is processed later at post build request processing
|
||||
@@ -198,7 +208,8 @@ const setUserAgent = (request, value) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Set authentication
|
||||
* Set authentication credentials
|
||||
* Stores credentials temporarily for finalization in post-processing
|
||||
*/
|
||||
const setAuth = (request, value) => {
|
||||
if (typeof value !== 'string') {
|
||||
@@ -206,15 +217,45 @@ const setAuth = (request, value) => {
|
||||
}
|
||||
|
||||
const [username, password] = value.split(':');
|
||||
request.auth = {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: username || '',
|
||||
password: password || ''
|
||||
}
|
||||
|
||||
// Store credentials temporarily for finalization in post-processing
|
||||
request.authCredentials = {
|
||||
username: username || '',
|
||||
password: password || ''
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Finalize authentication object based on credentials and auth type flags
|
||||
*/
|
||||
const normalizeAuthProperties = (request) => {
|
||||
if (!request.authCredentials) {
|
||||
delete request.isDigestAuth;
|
||||
delete request.isNtlmAuth;
|
||||
return;
|
||||
}
|
||||
|
||||
const { username, password } = request.authCredentials;
|
||||
|
||||
// Determine auth mode based on flags
|
||||
let mode = 'basic';
|
||||
if (request.isDigestAuth) {
|
||||
mode = 'digest';
|
||||
} else if (request.isNtlmAuth) {
|
||||
mode = 'ntlm';
|
||||
}
|
||||
|
||||
request.auth = {
|
||||
mode: mode,
|
||||
[mode]: { username, password }
|
||||
};
|
||||
|
||||
// Clean up temporary properties
|
||||
delete request.authCredentials;
|
||||
delete request.isDigestAuth;
|
||||
delete request.isNtlmAuth;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set request method
|
||||
*/
|
||||
@@ -433,7 +474,7 @@ const convertDataToQueryString = (request) => {
|
||||
|
||||
/**
|
||||
* Post-build processing of request
|
||||
* Handles method conversion and query parameter processing
|
||||
* Handles method conversion, query parameter processing, and auth finalization
|
||||
*/
|
||||
const postBuildProcessRequest = (request) => {
|
||||
if (request.isQuery && request.data) {
|
||||
@@ -448,6 +489,8 @@ const postBuildProcessRequest = (request) => {
|
||||
}
|
||||
}
|
||||
|
||||
normalizeAuthProperties(request);
|
||||
|
||||
// if method is not set, set it to GET
|
||||
if (!request.method) {
|
||||
request.method = 'GET';
|
||||
|
||||
@@ -272,6 +272,101 @@ describe('parseCurlCommand', () => {
|
||||
urlWithoutQuery: 'https://api.example.com'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse digest authentication', () => {
|
||||
const result = parseCurlCommand(`
|
||||
curl --digest -u "myuser:mypass" https://api.example.com/digest
|
||||
`);
|
||||
|
||||
expect(result).toEqual({
|
||||
method: 'get',
|
||||
auth: {
|
||||
mode: 'digest',
|
||||
digest: {
|
||||
username: 'myuser',
|
||||
password: 'mypass'
|
||||
}
|
||||
},
|
||||
url: 'https://api.example.com/digest',
|
||||
urlWithoutQuery: 'https://api.example.com/digest'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse digest authentication with --user flag', () => {
|
||||
const result = parseCurlCommand(`
|
||||
curl --digest --user "admin:secret" https://api.example.com/secure
|
||||
`);
|
||||
|
||||
expect(result).toEqual({
|
||||
method: 'get',
|
||||
auth: {
|
||||
mode: 'digest',
|
||||
digest: {
|
||||
username: 'admin',
|
||||
password: 'secret'
|
||||
}
|
||||
},
|
||||
url: 'https://api.example.com/secure',
|
||||
urlWithoutQuery: 'https://api.example.com/secure'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse NTLM authentication', () => {
|
||||
const result = parseCurlCommand(`
|
||||
curl --ntlm -u "myuser:mypass" https://api.example.com/ntlm
|
||||
`);
|
||||
|
||||
expect(result).toEqual({
|
||||
method: 'get',
|
||||
auth: {
|
||||
mode: 'ntlm',
|
||||
ntlm: {
|
||||
username: 'myuser',
|
||||
password: 'mypass'
|
||||
}
|
||||
},
|
||||
url: 'https://api.example.com/ntlm',
|
||||
urlWithoutQuery: 'https://api.example.com/ntlm'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse NTLM authentication with --user flag', () => {
|
||||
const result = parseCurlCommand(`
|
||||
curl --ntlm --user "domain\\username:password" https://api.example.com/ntlm
|
||||
`);
|
||||
|
||||
expect(result).toEqual({
|
||||
method: 'get',
|
||||
auth: {
|
||||
mode: 'ntlm',
|
||||
ntlm: {
|
||||
username: 'domain\\username',
|
||||
password: 'password'
|
||||
}
|
||||
},
|
||||
url: 'https://api.example.com/ntlm',
|
||||
urlWithoutQuery: 'https://api.example.com/ntlm'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle digest auth flag before -u flag', () => {
|
||||
const result = parseCurlCommand(`
|
||||
curl -u "user:pass" --digest https://api.example.com
|
||||
`);
|
||||
|
||||
expect(result).toEqual({
|
||||
method: 'get',
|
||||
auth: {
|
||||
mode: 'digest',
|
||||
digest: {
|
||||
username: 'user',
|
||||
password: 'pass'
|
||||
}
|
||||
},
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Data', () => {
|
||||
|
||||
Reference in New Issue
Block a user