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 03c4cd973a..827e6472ab 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'; + // 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 @@ -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/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 e20f35a22d..b2d3082c4b 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,3 +554,82 @@ describe('generateSnippet with edge-case bodies', () => { 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'/); + }); +}); 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 262f73ea78..f571c56b30 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 e7493b5f29..dc6f8c93dd 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 || '' } @@ -433,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) { @@ -448,6 +468,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 8701d980ca..6335c35ace 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', () => {