Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
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';

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
Expand All @@ -13,6 +36,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);
Expand Down Expand Up @@ -40,7 +69,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'/);
});
});
19 changes: 18 additions & 1 deletion packages/bruno-app/src/utils/curl/curl-to-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,31 @@ const curlToJson = (curlCommand) => {
}

if (request.auth) {
if (request.auth.mode === 'basic') {
const authMode = request.auth.mode;
if (authMode === 'basic') {
requestJson.auth = {
mode: 'basic',
basic: {
username: repr(request.auth.basic?.username),
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)
}
};
}
}

Expand Down
42 changes: 39 additions & 3 deletions packages/bruno-app/src/utils/curl/parse-curl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -199,16 +209,26 @@ 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') {
return;
}

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 || ''
}
Expand Down Expand Up @@ -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) {
Expand All @@ -449,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';
Expand Down
100 changes: 100 additions & 0 deletions packages/bruno-app/src/utils/curl/parse-curl.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading