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 b7a573baf..06e02a921 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,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) { 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 3c7e2c5ef..6101adab3 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 @@ -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'/); + }); +}); 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 262f73ea7..f571c56b3 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 e7493b5f2..24dd9334a 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 @@ -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'; 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 8701d980c..7f24aba78 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,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', () => {