From d2e58e16a8a5098a57b021c467e3ca6c8e53ccda Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Wed, 3 Dec 2025 18:41:06 +0530 Subject: [PATCH 1/3] fix: cURL auth import for digest and ntlm --- .../utils/snippet-generator.js | 38 ++++++++++++++++++- .../bruno-app/src/utils/curl/curl-to-json.js | 19 +++++++++- .../bruno-app/src/utils/curl/parse-curl.js | 24 +++++++++++- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js index 09a3dd818b..db81fe84f5 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js @@ -1,9 +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 { interpolateHeaders, interpolateBody } from './interpolation'; import { get } from 'lodash'; +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'; + + // Insert auth flags after 'curl' command + // Handle both 'curl' and 'curl.exe' cases + const curlMatch = curlCommand.match(/^(curl(?:\.exe)?)/i); + if (curlMatch) { + const curlCmd = curlMatch[1]; + const restOfCommand = curlCommand.slice(curlCmd.length); + return `${curlCmd} ${authFlag} --user '${credentials}'${restOfCommand}`; + } + } + + return curlCommand; +}; + const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => { try { // Get HTTPSnippet dynamically so mocks can be applied in tests @@ -13,6 +38,12 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false 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 +71,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) { diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index 24269f9a99..a07cc57a60 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -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) + } + }; } } diff --git a/packages/bruno-app/src/utils/curl/parse-curl.js b/packages/bruno-app/src/utils/curl/parse-curl.js index cf499d187f..4c3c04d8b9 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.js @@ -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 @@ -199,6 +209,7 @@ const setUserAgent = (request, value) => { /** * Set authentication + * Supports basic, digest, and NTLM auth based on --digest/--ntlm flags */ const setAuth = (request, value) => { if (typeof value !== 'string') { @@ -206,9 +217,18 @@ const setAuth = (request, value) => { } const [username, password] = value.split(':'); + + // Determine auth mode based on flags + let mode = 'basic'; + if (request.isDigestAuth) { + mode = 'digest'; + } else if (request.isNtlmAuth) { + mode = 'ntlm'; + } + request.auth = { - mode: 'basic', - basic: { + mode: mode, + [mode]: { username: username || '', password: password || '' } From 742ffbf64bd71a504c0bcaf7ff348e8f433d1e19 Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Wed, 3 Dec 2025 18:56:56 +0530 Subject: [PATCH 2/3] add: unit test --- .../utils/snippet-generator.js | 2 - .../utils/snippet-generator.spec.js | 79 ++++++++++++++ .../bruno-app/src/utils/curl/parse-curl.js | 18 +++- .../src/utils/curl/parse-curl.spec.js | 100 ++++++++++++++++++ 4 files changed, 196 insertions(+), 3 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js index db81fe84f5..1253f58eae 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js @@ -16,8 +16,6 @@ const addCurlAuthFlags = (curlCommand, auth) => { const credentials = password ? `${username}:${password}` : username; const authFlag = authMode === 'digest' ? '--digest' : '--ntlm'; - // Insert auth flags after 'curl' command - // Handle both 'curl' and 'curl.exe' cases const curlMatch = curlCommand.match(/^(curl(?:\.exe)?)/i); if (curlMatch) { const curlCmd = curlMatch[1]; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js index 359fba0a7a..1eae0be99a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js @@ -554,4 +554,83 @@ describe('generateSnippet with edge-case bodies', () => { const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: true }); expect(result).toMatch(/^curl -X POST/); }); +}); + +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'/); + }); }); \ No newline at end of file diff --git a/packages/bruno-app/src/utils/curl/parse-curl.js b/packages/bruno-app/src/utils/curl/parse-curl.js index 4c3c04d8b9..9c382ae659 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.js @@ -453,7 +453,7 @@ const convertDataToQueryString = (request) => { /** * Post-build processing of request - * Handles method conversion and query parameter processing + * Handles method conversion, query parameter processing, and auth mode correction */ const postBuildProcessRequest = (request) => { if (request.isQuery && request.data) { @@ -469,6 +469,22 @@ const postBuildProcessRequest = (request) => { } } + // Fix auth mode if digest/ntlm flag was set after -u flag + // (handles case where -u comes before --digest/--ntlm in the command) + if (request.auth && request.isDigestAuth && request.auth.mode !== 'digest') { + const { username, password } = request.auth[request.auth.mode] || {}; + request.auth = { + mode: 'digest', + digest: { username: username || '', password: password || '' } + }; + } else if (request.auth && request.isNtlmAuth && request.auth.mode !== 'ntlm') { + const { username, password } = request.auth[request.auth.mode] || {}; + request.auth = { + mode: 'ntlm', + ntlm: { username: username || '', password: password || '' } + }; + } + // if method is not set, set it to GET if (!request.method) { request.method = 'GET'; diff --git a/packages/bruno-app/src/utils/curl/parse-curl.spec.js b/packages/bruno-app/src/utils/curl/parse-curl.spec.js index e0da77d37a..a145e1db48 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.spec.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.spec.js @@ -272,6 +272,106 @@ 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' + } + }, + isDigestAuth: true, + 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' + } + }, + isDigestAuth: true, + 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' + } + }, + isNtlmAuth: true, + 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' + } + }, + isNtlmAuth: true, + 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' + } + }, + isDigestAuth: true, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); }); describe('Form Data', () => { From d24adb5e987250879bfce4ba661a40a10c591201 Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Wed, 3 Dec 2025 22:05:19 +0530 Subject: [PATCH 3/3] fix --- .../GenerateCodeItem/utils/snippet-generator.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js index 1253f58eae..01de2b4d3e 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js @@ -15,12 +15,14 @@ const addCurlAuthFlags = (curlCommand, auth) => { 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 '${credentials}'${restOfCommand}`; + return `${curlCmd} ${authFlag} --user '${escapedCredentials}'${restOfCommand}`; } }