fix: cURL auth import for digest and ntlm (#6292)

This commit is contained in:
Pooja
2026-01-30 13:05:28 +05:30
committed by GitHub
parent ca4d0dd40b
commit 6ca5c71f7a
5 changed files with 281 additions and 10 deletions

View File

@@ -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) {

View File

@@ -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'/);
});
});

View File

@@ -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)
}
};
}
}

View File

@@ -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';

View File

@@ -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', () => {