diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js
index cd69d08cc..55b0bbdad 100644
--- a/packages/bruno-app/src/providers/App/useTelemetry.js
+++ b/packages/bruno-app/src/providers/App/useTelemetry.js
@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
- version: '1.30.0'
+ version: '1.30.1'
}
});
};
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index 054b4fbd4..777a194ab 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -43,6 +43,7 @@ import { resolveRequestFilename } from 'utils/common/platform';
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
import { name } from 'file-loader';
+import slash from 'utils/common/slash';
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
@@ -401,7 +402,7 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
}
const { ipcRenderer } = window;
- ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
+ ipcRenderer.invoke('renderer:rename-item', slash(item.pathname), newPathname, newName).then(resolve).catch(reject);
});
};
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index 34a6c6af9..b7ef2f86e 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -477,6 +477,10 @@ export const collectionsSlice = createSlice({
item.draft.request.auth.mode = 'oauth2';
item.draft.request.auth.oauth2 = action.payload.content;
break;
+ case 'wsse':
+ item.draft.request.auth.mode = 'wsse';
+ item.draft.request.auth.wsse = action.payload.content;
+ break;
case 'apikey':
item.draft.request.auth.mode = 'apikey';
item.draft.request.auth.apikey = action.payload.content;
@@ -1141,6 +1145,9 @@ export const collectionsSlice = createSlice({
case 'oauth2':
set(collection, 'root.request.auth.oauth2', action.payload.content);
break;
+ case 'wsse':
+ set(collection, 'root.request.auth.wsse', action.payload.content);
+ break;
case 'apikey':
set(collection, 'root.request.auth.apikey', action.payload.content);
break;
diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js
index fa0738503..9bbd0eea9 100644
--- a/packages/bruno-app/src/utils/codegenerator/har.js
+++ b/packages/bruno-app/src/utils/codegenerator/har.js
@@ -31,6 +31,7 @@ const createHeaders = (request, headers) => {
if (contentType !== '') {
enabledHeaders.push({ name: 'content-type', value: contentType });
}
+
return enabledHeaders;
};
@@ -43,7 +44,14 @@ const createQuery = (queryParams = []) => {
}));
};
-const createPostData = (body) => {
+const createPostData = (body, type) => {
+ if (type === 'graphql-request') {
+ return {
+ mimeType: 'application/json',
+ text: JSON.stringify(body[body.mode])
+ };
+ }
+
const contentType = createContentType(body.mode);
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
return {
@@ -64,7 +72,7 @@ const createPostData = (body) => {
}
};
-export const buildHarRequest = ({ request, headers }) => {
+export const buildHarRequest = ({ request, headers, type }) => {
return {
method: request.method,
url: encodeURI(request.url),
@@ -72,7 +80,7 @@ export const buildHarRequest = ({ request, headers }) => {
cookies: [],
headers: createHeaders(request, headers),
queryString: createQuery(request.params),
- postData: createPostData(request.body),
+ postData: createPostData(request.body, type),
headersSize: 0,
bodySize: 0
};
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 99d9b269c..ea8712be5 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -379,7 +379,12 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
placement: get(si.request, 'auth.apikey.placement', 'header')
};
break;
-
+ case 'wsse':
+ di.request.auth.wsse = {
+ username: get(si.request, 'auth.wsse.username', ''),
+ password: get(si.request, 'auth.wsse.password', '')
+ };
+ break;
default:
break;
}
@@ -669,6 +674,10 @@ export const humanizeRequestAuthMode = (mode) => {
label = 'OAuth 2.0';
break;
}
+ case 'wsse': {
+ label = 'WSSE Auth';
+ break;
+ }
case 'apikey': {
label = 'API Key';
break;
diff --git a/packages/bruno-app/src/utils/importers/insomnia-collection.js b/packages/bruno-app/src/utils/importers/insomnia-collection.js
index 0fec995ca..ae74f1613 100644
--- a/packages/bruno-app/src/utils/importers/insomnia-collection.js
+++ b/packages/bruno-app/src/utils/importers/insomnia-collection.js
@@ -174,7 +174,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
} else if (mimeType === 'text/plain') {
brunoRequestItem.request.body.mode = 'text';
brunoRequestItem.request.body.text = request.body.text;
- } else if (mimeType === 'text/xml') {
+ } else if (mimeType === 'text/xml' || mimeType === 'application/xml') {
brunoRequestItem.request.body.mode = 'xml';
brunoRequestItem.request.body.xml = request.body.text;
} else if (mimeType === 'application/graphql') {
diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js
index 3bfe8394f..fd25b7f51 100644
--- a/packages/bruno-app/src/utils/importers/openapi-collection.js
+++ b/packages/bruno-app/src/utils/importers/openapi-collection.js
@@ -32,7 +32,7 @@ const readFile = (files) => {
const ensureUrl = (url) => {
// emoving multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
- return url.replace(/(^\w+:|^)\/{2,}/, '$1/');
+ return url.replace(/([^:])\/{2,}/g, '$1/');
};
const buildEmptyJsonBody = (bodySchema) => {
diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js
index 4486e0efc..053a55723 100644
--- a/packages/bruno-app/src/utils/importers/postman-collection.js
+++ b/packages/bruno-app/src/utils/importers/postman-collection.js
@@ -54,13 +54,54 @@ const convertV21Auth = (array) => {
}, {});
};
+const constructUrlFromParts = (url) => {
+ const { protocol = 'http', host, path, port, query, hash } = url || {};
+ const hostStr = Array.isArray(host) ? host.filter(Boolean).join('.') : host || '';
+ const pathStr = Array.isArray(path) ? path.filter(Boolean).join('/') : path || '';
+ const portStr = port ? `:${port}` : '';
+ const queryStr =
+ query && Array.isArray(query) && query.length > 0
+ ? `?${query
+ .filter((q) => q.key)
+ .map((q) => `${q.key}=${q.value || ''}`)
+ .join('&')}`
+ : '';
+ const urlStr = `${protocol}://${hostStr}${portStr}${pathStr ? `/${pathStr}` : ''}${queryStr}`;
+ return urlStr;
+};
+
+const constructUrl = (url) => {
+ if (!url) return '';
+
+ if (typeof url === 'string') {
+ return url;
+ }
+
+ if (typeof url === 'object') {
+ const { raw } = url;
+
+ if (raw && typeof raw === 'string') {
+ // If the raw URL contains url-fragments remove it
+ if (raw.includes('#')) {
+ return raw.split('#')[0]; // Returns the part of raw URL without the url-fragment part.
+ }
+ return raw;
+ }
+
+ // If no raw value exists, construct the URL from parts
+ return constructUrlFromParts(url);
+ }
+
+ return '';
+};
+
let translationLog = {};
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
brunoParent.items = brunoParent.items || [];
const folderMap = {};
const requestMap = {};
-
+
each(item, (i) => {
if (isItemAFolder(i)) {
const baseFolderName = i.name;
@@ -86,20 +127,15 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
} else {
if (i.request) {
const baseRequestName = i.name;
- let requestName = baseRequestName;
+ let requestName = baseRequestName;
let count = 1;
while (requestMap[requestName]) {
requestName = `${baseRequestName}_${count}`;
count++;
}
-
- let url = '';
- if (typeof i.request.url === 'string') {
- url = i.request.url;
- } else {
- url = get(i, 'request.url.raw') || '';
- }
+
+ const url = constructUrl(i.request.url);
const brunoRequestItem = {
uid: uuid(),
@@ -107,7 +143,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
type: 'http-request',
request: {
url: url,
- method: i.request.method,
+ method: i?.request?.method?.toUpperCase(),
auth: {
mode: 'none',
basic: null,
@@ -313,12 +349,17 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
});
});
- each(get(i, 'request.url.variable'), (param) => {
+ each(get(i, 'request.url.variable', []), (param) => {
+ if (!param.key) {
+ // If no key, skip this iteration and discard the param
+ return;
+ }
+
brunoRequestItem.request.params.push({
uid: uuid(),
name: param.key,
- value: param.value,
- description: param.description,
+ value: param.value ?? '',
+ description: param.description ?? '',
type: 'path',
enabled: true
});
diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js
index c35456993..0253c10cd 100644
--- a/packages/bruno-cli/src/runner/interpolate-vars.js
+++ b/packages/bruno-cli/src/runner/interpolate-vars.js
@@ -1,5 +1,6 @@
const { interpolate } = require('@usebruno/common');
const { each, forOwn, cloneDeep, find } = require('lodash');
+const FormData = require('form-data');
const getContentType = (headers = {}) => {
let contentType = '';
@@ -78,6 +79,14 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
request.data = JSON.parse(parsed);
} catch (err) {}
}
+ } else if (contentType === 'multipart/form-data') {
+ if (typeof request.data === 'object' && !(request?.data instanceof FormData)) {
+ try {
+ let parsed = JSON.stringify(request.data);
+ parsed = _interpolate(parsed);
+ request.data = JSON.parse(parsed);
+ } catch (err) {}
+ }
} else {
request.data = _interpolate(request.data);
}
@@ -113,7 +122,8 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
})
.join('');
- request.url = url.origin + interpolatedUrlPath + url.search;
+ const trailingSlash = url.pathname.endsWith('/') ? '/' : '';
+ request.url = url.origin + interpolatedUrlPath + trailingSlash + url.search;
}
if (request.proxy) {
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index e30f8337f..d6688a1ff 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -2,6 +2,7 @@ const { get, each, filter } = require('lodash');
const fs = require('fs');
var JSONbig = require('json-bigint');
const decomment = require('decomment');
+const crypto = require('node:crypto');
const prepareRequest = (request, collectionRoot) => {
const headers = {};
@@ -69,6 +70,24 @@ const prepareRequest = (request, collectionRoot) => {
if (request.auth.mode === 'bearer') {
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
}
+
+ if (request.auth.mode === 'wsse') {
+ const username = get(request, 'auth.wsse.username', '');
+ const password = get(request, 'auth.wsse.password', '');
+
+ const ts = new Date().toISOString();
+ const nonce = crypto.randomBytes(16).toString('base64');
+
+ // Create the password digest using SHA-256
+ const hash = crypto.createHash('sha256');
+ hash.update(nonce + ts + password);
+ const digest = hash.digest('base64');
+
+ // Construct the WSSE header
+ axiosRequest.headers[
+ 'X-WSSE'
+ ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
+ }
}
request.body = request.body || {};
@@ -120,16 +139,10 @@ const prepareRequest = (request, collectionRoot) => {
}
if (request.body.mode === 'multipartForm') {
+ axiosRequest.headers['content-type'] = 'multipart/form-data';
const params = {};
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
- each(enabledParams, (p) => {
- if (p.type === 'file') {
- params[p.name] = p.value.map((path) => fs.createReadStream(path));
- } else {
- params[p.name] = p.value;
- }
- });
- axiosRequest.headers['content-type'] = 'multipart/form-data';
+ each(enabledParams, (p) => (params[p.name] = p.value));
axiosRequest.data = params;
}
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index b260f6be9..cb59c78ba 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -19,6 +19,7 @@ const { makeAxiosInstance } = require('../utils/axios-instance');
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
const path = require('path');
+const { createFormData } = require('../utils/common');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const onConsoleLog = (type, args) => {
@@ -42,24 +43,11 @@ const runSingleRequest = async function (
request = prepareRequest(bruJson.request, collectionRoot);
+ request.__bruno__executionMode = 'cli';
+
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = runtime;
- // make axios work in node using form data
- // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
- if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
- const form = new FormData();
- forOwn(request.data, (value, key) => {
- if (value instanceof Array) {
- each(value, (v) => form.append(key, v));
- } else {
- form.append(key, value);
- }
- });
- extend(request.headers, form.getHeaders());
- request.data = form;
- }
-
// run pre request script
const requestScriptFile = compact([
get(collectionRoot, 'request.script.req'),
@@ -195,6 +183,14 @@ const runSingleRequest = async function (
request.data = qs.stringify(request.data);
}
+ if (request?.headers?.['content-type'] === 'multipart/form-data') {
+ if (!(request?.data instanceof FormData)) {
+ let form = createFormData(request.data, collectionPath);
+ request.data = form;
+ extend(request.headers, form.getHeaders());
+ }
+ }
+
let response, responseTime;
try {
// run request
diff --git a/packages/bruno-cli/src/utils/common.js b/packages/bruno-cli/src/utils/common.js
index 704928022..16c2d1a7b 100644
--- a/packages/bruno-cli/src/utils/common.js
+++ b/packages/bruno-cli/src/utils/common.js
@@ -1,3 +1,8 @@
+const fs = require('fs');
+const FormData = require('form-data');
+const { forOwn } = require('lodash');
+const path = require('path');
+
const lpad = (str, width) => {
let paddedStr = str;
while (paddedStr.length < width) {
@@ -14,7 +19,33 @@ const rpad = (str, width) => {
return paddedStr;
};
+const createFormData = (datas, collectionPath) => {
+ // make axios work in node using form data
+ // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
+ const form = new FormData();
+ forOwn(datas, (value, key) => {
+ if (typeof value == 'string') {
+ form.append(key, value);
+ return;
+ }
+
+ const filePaths = value || [];
+ filePaths?.forEach?.((filePath) => {
+ let trimmedFilePath = filePath.trim();
+
+ if (!path.isAbsolute(trimmedFilePath)) {
+ trimmedFilePath = path.join(collectionPath, trimmedFilePath);
+ }
+
+ form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
+ });
+ });
+ return form;
+};
+
+
module.exports = {
lpad,
- rpad
+ rpad,
+ createFormData
};
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index ad1b2c53f..b158989aa 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -1,5 +1,5 @@
{
- "version": "v1.30.0",
+ "version": "v1.30.1",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 945c21559..950902ece 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -16,6 +16,8 @@ const {
sanitizeDirectoryName,
isWSLPath,
normalizeWslPath,
+ normalizeAndResolvePath,
+ safeToRename
} = require('../utils/filesystem');
const { openCollectionDialog } = require('../app/collections');
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
@@ -296,7 +298,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
- if (fs.existsSync(newEnvFilePath)) {
+ if (!safeToRename(envFilePath, newEnvFilePath)) {
throw new Error(`environment: ${newEnvFilePath} already exists`);
}
@@ -329,21 +331,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
try {
// Normalize paths if they are WSL paths
- if (isWSLPath(oldPath)) {
- oldPath = normalizeWslPath(oldPath);
- }
- if (isWSLPath(newPath)) {
- newPath = normalizeWslPath(newPath);
- }
+ oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath);
+ newPath = isWSLPath(newPath) ? normalizeWslPath(newPath) : normalizeAndResolvePath(newPath);
+ // Check if the old path exists
if (!fs.existsSync(oldPath)) {
throw new Error(`path: ${oldPath} does not exist`);
}
- if (fs.existsSync(newPath)) {
- throw new Error(`path: ${oldPath} already exists`);
+
+ if (!safeToRename(oldPath, newPath)) {
+ throw new Error(`path: ${newPath} already exists`);
}
- // if its directory, rename and return
if (isDirectory(oldPath)) {
const bruFilesAtSource = await searchForBruFiles(oldPath);
@@ -364,12 +363,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const jsonData = bruToJson(data);
jsonData.name = newName;
-
moveRequestUid(oldPath, newPath);
const content = jsonToBru(jsonData);
- await writeFile(newPath, content);
await fs.unlinkSync(oldPath);
+ await writeFile(newPath, content);
+
+ return newPath;
} catch (error) {
return Promise.reject(error);
}
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index d7ed96d28..328232e31 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -9,7 +9,7 @@ const decomment = require('decomment');
const contentDispositionParser = require('content-disposition');
const mime = require('mime-types');
const { ipcMain } = require('electron');
-const { isUndefined, isNull, each, get, compact, cloneDeep } = require('lodash');
+const { isUndefined, isNull, each, get, compact, cloneDeep, forOwn, extend } = require('lodash');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
const prepareRequest = require('./prepare-request');
const prepareCollectionRequest = require('./prepare-collection-request');
@@ -37,6 +37,8 @@ const {
} = require('./oauth2-helper');
const Oauth2Store = require('../../store/oauth2');
const iconv = require('iconv-lite');
+const FormData = require('form-data');
+const { createFormData } = prepareRequest;
const safeStringifyJSON = (data) => {
try {
@@ -423,6 +425,14 @@ const registerNetworkIpc = (mainWindow) => {
request.data = qs.stringify(request.data);
}
+ if (request.headers['content-type'] === 'multipart/form-data') {
+ if (!(request.data instanceof FormData)) {
+ let form = createFormData(request.data, collectionPath);
+ request.data = form;
+ extend(request.headers, form.getHeaders());
+ }
+ }
+
return scriptResult;
};
@@ -515,6 +525,7 @@ const registerNetworkIpc = (mainWindow) => {
const collectionRoot = get(collection, 'root', {});
const request = prepareRequest(item, collection);
+ request.__bruno__executionMode = 'standalone';
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
@@ -707,6 +718,7 @@ const registerNetworkIpc = (mainWindow) => {
const collectionRoot = get(collection, 'root', {});
const _request = collectionRoot?.request;
const request = prepareCollectionRequest(_request, collectionRoot, collectionPath);
+ request.__bruno__executionMode = 'standalone';
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
@@ -950,6 +962,8 @@ const registerNetworkIpc = (mainWindow) => {
});
const request = prepareRequest(item, collection);
+ request.__bruno__executionMode = 'runner';
+
const requestUid = uuid();
const processEnvVars = getProcessEnvVars(collectionUid);
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index b6aeaa078..5ea2bf7f4 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -1,5 +1,6 @@
const { interpolate } = require('@usebruno/common');
const { each, forOwn, cloneDeep, find } = require('lodash');
+const FormData = require('form-data');
const getContentType = (headers = {}) => {
let contentType = '';
@@ -76,6 +77,14 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
request.data = JSON.parse(parsed);
} catch (err) {}
}
+ } else if (contentType === 'multipart/form-data') {
+ if (typeof request.data === 'object' && !(request.data instanceof FormData)) {
+ try {
+ let parsed = JSON.stringify(request.data);
+ parsed = _interpolate(parsed);
+ request.data = JSON.parse(parsed);
+ } catch (err) {}
+ }
} else {
request.data = _interpolate(request.data);
}
@@ -111,7 +120,8 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
})
.join('');
- request.url = url.origin + urlPathnameInterpolatedWithPathParams + url.search;
+ const trailingSlash = url.pathname.endsWith('/') ? '/' : '';
+ request.url = url.origin + urlPathnameInterpolatedWithPathParams + trailingSlash + url.search;
}
if (request.proxy) {
@@ -206,6 +216,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
request.digestConfig.password = _interpolate(request.digestConfig.password) || '';
}
+ // interpolate vars for wsse auth
+ if (request.wsse) {
+ request.wsse.username = _interpolate(request.wsse.username) || '';
+ request.wsse.password = _interpolate(request.wsse.password) || '';
+ }
+
return request;
};
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 75b0f2c0e..0bac42af9 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -1,9 +1,10 @@
const os = require('os');
-const { get, each, filter, extend, compact } = require('lodash');
+const { get, each, filter, compact, forOwn } = require('lodash');
const decomment = require('decomment');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
+const crypto = require('node:crypto');
const { getTreePathFromCollectionToItem } = require('../../utils/collection');
const { buildFormUrlEncodedPayload } = require('../../utils/common');
@@ -165,27 +166,26 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
}
};
-const parseFormData = (datas, collectionPath) => {
+const createFormData = (datas, collectionPath) => {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
const form = new FormData();
- datas.forEach((item) => {
- const value = item.value;
- const name = item.name;
- if (item.type === 'file') {
- const filePaths = value || [];
- filePaths.forEach((filePath) => {
- let trimmedFilePath = filePath.trim();
-
- if (!path.isAbsolute(trimmedFilePath)) {
- trimmedFilePath = path.join(collectionPath, trimmedFilePath);
- }
-
- form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
- });
- } else {
- form.append(name, value);
+ forOwn(datas, (value, key) => {
+ if (typeof value == 'string') {
+ form.append(key, value);
+ return;
}
+
+ const filePaths = value || [];
+ filePaths?.forEach?.((filePath) => {
+ let trimmedFilePath = filePath.trim();
+
+ if (!path.isAbsolute(trimmedFilePath)) {
+ trimmedFilePath = path.join(collectionPath, trimmedFilePath);
+ }
+
+ form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
+ });
});
return form;
};
@@ -219,6 +219,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
password: get(collectionAuth, 'digest.password')
};
break;
+ case 'wsse':
+ const username = get(request, 'auth.wsse.username', '');
+ const password = get(request, 'auth.wsse.password', '');
+
+ const ts = new Date().toISOString();
+ const nonce = crypto.randomBytes(16).toString('base64');
+
+ // Create the password digest using SHA-256
+ const hash = crypto.createHash('sha256');
+ hash.update(nonce + ts + password);
+ const digest = hash.digest('base64');
+
+ // Construct the WSSE header
+ axiosRequest.headers[
+ 'X-WSSE'
+ ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
+ break;
case 'apikey':
const apiKeyAuth = get(collectionAuth, 'apikey');
if (apiKeyAuth.placement === 'header') {
@@ -296,6 +313,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
break;
}
break;
+ case 'wsse':
+ const username = get(request, 'auth.wsse.username', '');
+ const password = get(request, 'auth.wsse.password', '');
+
+ const ts = new Date().toISOString();
+ const nonce = crypto.randomBytes(16).toString('base64');
+
+ // Create the password digest using SHA-256
+ const hash = crypto.createHash('sha256');
+ hash.update(nonce + ts + password);
+ const digest = hash.digest('base64');
+
+ // Construct the WSSE header
+ axiosRequest.headers[
+ 'X-WSSE'
+ ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
+ break;
case 'apikey':
const apiKeyAuth = get(request, 'auth.apikey');
if (apiKeyAuth.placement === 'header') {
@@ -400,10 +434,11 @@ const prepareRequest = (item, collection) => {
}
if (request.body.mode === 'multipartForm') {
+ axiosRequest.headers['content-type'] = 'multipart/form-data';
+ const params = {};
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
- const form = parseFormData(enabledParams, collectionPath);
- extend(axiosRequest.headers, form.getHeaders());
- axiosRequest.data = form;
+ each(enabledParams, (p) => (params[p.name] = p.value));
+ axiosRequest.data = params;
}
if (request.body.mode === 'graphql') {
@@ -433,3 +468,4 @@ const prepareRequest = (item, collection) => {
module.exports = prepareRequest;
module.exports.setAuthHeaders = setAuthHeaders;
+module.exports.createFormData = createFormData;
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index 752cb339c..0263939ae 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
const fsPromises = require('fs/promises');
const { dialog } = require('electron');
const isValidPathname = require('is-valid-path');
+const os = require('os');
const exists = async (p) => {
try {
@@ -155,12 +156,34 @@ const searchForBruFiles = (dir) => {
return searchForFiles(dir, '.bru');
};
-// const isW
-
const sanitizeDirectoryName = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
};
+const safeToRename = (oldPath, newPath) => {
+ try {
+ // If the new path doesn't exist, it's safe to rename
+ if (!fs.existsSync(newPath)) {
+ return true;
+ }
+
+ const oldStat = fs.statSync(oldPath);
+ const newStat = fs.statSync(newPath);
+
+ if (os.platform() === 'win32') {
+ // Windows-specific comparison:
+ // Check if both files have the same birth time, size (Since, Win FAT-32 doesn't use inodes)
+
+ return oldStat.birthtimeMs === newStat.birthtimeMs && oldStat.size === newStat.size;
+ }
+ // Unix/Linux/MacOS: Check inode to see if they are the same file
+ return oldStat.ino === newStat.ino;
+ } catch (error) {
+ console.error(`Error checking file rename safety for ${oldPath} and ${newPath}:`, error);
+ return false;
+ }
+};
+
module.exports = {
isValidPathname,
exists,
@@ -180,5 +203,6 @@ module.exports = {
chooseFileToSave,
searchForFiles,
searchForBruFiles,
- sanitizeDirectoryName
+ sanitizeDirectoryName,
+ safeToRename
};
diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js
index cf5f59aca..32e40c19e 100644
--- a/packages/bruno-js/src/bruno-request.js
+++ b/packages/bruno-js/src/bruno-request.js
@@ -43,7 +43,6 @@ class BrunoRequest {
getMethod() {
return this.req.method;
}
-
getAuthMode() {
if (this.req?.oauth2) {
return 'oauth2';
@@ -55,6 +54,8 @@ class BrunoRequest {
return 'awsv4';
} else if (this.req?.digestConfig) {
return 'digest';
+ } else if (this.headers?.['X-WSSE'] || this.req?.auth?.username) {
+ return 'wsse';
} else {
return 'none';
}
@@ -172,6 +173,10 @@ class BrunoRequest {
disableParsingResponseJson() {
this.req.__brunoDisableParsingResponseJson = true;
}
+
+ getExecutionMode() {
+ return this.req.__bruno__executionMode;
+ }
}
module.exports = BrunoRequest;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
index 1edfaaadb..e3f364fe7 100644
--- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
@@ -111,6 +111,12 @@ const addBrunoRequestShimToContext = (vm, req) => {
vm.setProp(reqObject, 'disableParsingResponseJson', disableParsingResponseJson);
disableParsingResponseJson.dispose();
+ let getExecutionMode = vm.newFunction('getExecutionMode', function () {
+ return marshallToVm(req.getExecutionMode(), vm);
+ });
+ vm.setProp(reqObject, 'getExecutionMode', getExecutionMode);
+ getExecutionMode.dispose();
+
vm.setProp(vm.global, 'req', reqObject);
reqObject.dispose();
};
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 84890c92f..c84d36d07 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils');
*/
const grammar = ohm.grammar(`Bru {
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
- auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authapikey
+ auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authwsse | authapikey
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
bodyforms = bodyformurlencoded | bodymultipart
params = paramspath | paramsquery
@@ -88,6 +88,7 @@ const grammar = ohm.grammar(`Bru {
authbearer = "auth:bearer" dictionary
authdigest = "auth:digest" dictionary
authOAuth2 = "auth:oauth2" dictionary
+ authwsse = "auth:wsse" dictionary
authapikey = "auth:apikey" dictionary
body = "body" st* "{" nl* textblock tagend
@@ -484,6 +485,23 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
+ authwsse(_1, dictionary) {
+ const auth = mapPairListToKeyValPairs(dictionary.ast, false);
+
+ const userKey = _.find(auth, { name: 'username' });
+ const secretKey = _.find(auth, { name: 'password' });
+ const username = userKey ? userKey.value : '';
+ const password = secretKey ? secretKey.value : '';
+
+ return {
+ auth: {
+ wsse: {
+ username,
+ password
+ }
+ }
+ };
+ },
authapikey(_1, dictionary) {
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js
index 513c4102d..5180f0193 100644
--- a/packages/bruno-lang/v2/src/collectionBruToJson.js
+++ b/packages/bruno-lang/v2/src/collectionBruToJson.js
@@ -4,7 +4,7 @@ const { outdentString } = require('../../v1/src/utils');
const grammar = ohm.grammar(`Bru {
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
- auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authapikey
+ auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authwsse | authapikey
nl = "\\r"? "\\n"
st = " " | "\\t"
@@ -43,6 +43,7 @@ const grammar = ohm.grammar(`Bru {
authbearer = "auth:bearer" dictionary
authdigest = "auth:digest" dictionary
authOAuth2 = "auth:oauth2" dictionary
+ authwsse = "auth:wsse" dictionary
authapikey = "auth:apikey" dictionary
script = scriptreq | scriptres
@@ -294,6 +295,21 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
+ authwsse(_1, dictionary) {
+ const auth = mapPairListToKeyValPairs(dictionary.ast, false);
+ const userKey = _.find(auth, { name: 'username' });
+ const secretKey = _.find(auth, { name: 'password' });
+ const username = userKey ? userKey.value : '';
+ const password = secretKey ? secretKey.value : '';
+ return {
+ auth: {
+ wsse: {
+ username,
+ password
+ }
+ }
+ }
+ },
authapikey(_1, dictionary) {
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 30bec13ef..8d3a5fdee 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -136,6 +136,15 @@ ${indentString(`username: ${auth?.basic?.username || ''}`)}
${indentString(`password: ${auth?.basic?.password || ''}`)}
}
+`;
+ }
+
+ if (auth && auth.wsse) {
+ bru += `auth:wsse {
+${indentString(`username: ${auth?.wsse?.username || ''}`)}
+${indentString(`password: ${auth?.wsse?.password || ''}`)}
+}
+
`;
}
diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
index 6462efb3c..8b162b7a6 100644
--- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js
+++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
@@ -94,6 +94,15 @@ ${indentString(`username: ${auth.basic.username}`)}
${indentString(`password: ${auth.basic.password}`)}
}
+`;
+ }
+
+ if (auth && auth.wsse) {
+ bru += `auth:wsse {
+${indentString(`username: ${auth.wsse.username}`)}
+${indentString(`password: ${auth.wsse.password}`)}
+}
+
`;
}
diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.bru b/packages/bruno-lang/v2/tests/fixtures/collection.bru
index 44a66c8dc..f11954ebf 100644
--- a/packages/bruno-lang/v2/tests/fixtures/collection.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/collection.bru
@@ -17,6 +17,11 @@ auth:basic {
password: secret
}
+auth:wsse {
+ username: john
+ password: secret
+}
+
auth:bearer {
token: 123
}
diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.json b/packages/bruno-lang/v2/tests/fixtures/collection.json
index 7bda2534d..102ee295c 100644
--- a/packages/bruno-lang/v2/tests/fixtures/collection.json
+++ b/packages/bruno-lang/v2/tests/fixtures/collection.json
@@ -31,6 +31,10 @@
"digest": {
"username": "john",
"password": "secret"
+ },
+ "wsse": {
+ "username": "john",
+ "password": "secret"
}
},
"vars": {
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru
index c4ff61558..1a3efeab7 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/request.bru
@@ -40,6 +40,11 @@ auth:basic {
password: secret
}
+auth:wsse {
+ username: john
+ password: secret
+}
+
auth:bearer {
token: 123
}
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
index d0bd996f6..24997a90c 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.json
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -83,6 +83,10 @@
"scope": "read write",
"state": "807061d5f0be",
"pkce": false
+ },
+ "wsse": {
+ "username": "john",
+ "password": "secret"
}
},
"body": {
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 2934a60d8..11561c528 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -106,6 +106,13 @@ const authBasicSchema = Yup.object({
.noUnknown(true)
.strict();
+const authWsseSchema = Yup.object({
+ username: Yup.string().nullable(),
+ password: Yup.string().nullable()
+})
+ .noUnknown(true)
+ .strict();
+
const authBearerSchema = Yup.object({
token: Yup.string().nullable()
})
@@ -119,6 +126,14 @@ const authDigestSchema = Yup.object({
.noUnknown(true)
.strict();
+const authApiKeySchema = Yup.object({
+ key: Yup.string().nullable(),
+ value: Yup.string().nullable(),
+ placement: Yup.string().oneOf(['header', 'queryparams']).nullable()
+})
+ .noUnknown(true)
+ .strict();
+
const oauth2Schema = Yup.object({
grantType: Yup.string()
.oneOf(['client_credentials', 'password', 'authorization_code'])
@@ -177,21 +192,16 @@ const oauth2Schema = Yup.object({
.noUnknown(true)
.strict();
-const authApiKeySchema = Yup.object({
- key: Yup.string().nullable(),
- value: Yup.string().nullable(),
- placement: Yup.string().oneOf(['header', 'queryparams']).nullable()
-});
-
const authSchema = Yup.object({
mode: Yup.string()
- .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2', 'apikey'])
+ .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2', 'wsse', 'apikey'])
.required('mode is required'),
awsv4: authAwsV4Schema.nullable(),
basic: authBasicSchema.nullable(),
bearer: authBearerSchema.nullable(),
digest: authDigestSchema.nullable(),
oauth2: oauth2Schema.nullable(),
+ wsse: authWsseSchema.nullable(),
apikey: authApiKeySchema.nullable()
})
.noUnknown(true)
diff --git a/packages/bruno-tests/collection/bruno.json b/packages/bruno-tests/collection/bruno.json
index b6d437bbb..ada36145a 100644
--- a/packages/bruno-tests/collection/bruno.json
+++ b/packages/bruno-tests/collection/bruno.json
@@ -15,7 +15,7 @@
"bypassProxy": ""
},
"scripts": {
- "moduleWhitelist": ["crypto", "buffer"],
+ "moduleWhitelist": ["crypto", "buffer", "form-data"],
"filesystemAccess": {
"allow": true
}
diff --git a/packages/bruno-tests/collection/bruno.png b/packages/bruno-tests/collection/bruno.png
new file mode 100644
index 000000000..c2a7f878f
Binary files /dev/null and b/packages/bruno-tests/collection/bruno.png differ
diff --git a/packages/bruno-tests/collection/echo/echo form-url-encoded.bru b/packages/bruno-tests/collection/echo/echo form-url-encoded.bru
new file mode 100644
index 000000000..a0d2f0afb
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/echo form-url-encoded.bru
@@ -0,0 +1,23 @@
+meta {
+ name: echo form-url-encoded
+ type: http
+ seq: 9
+}
+
+post {
+ url: {{echo-host}}
+ body: formUrlEncoded
+ auth: none
+}
+
+body:form-urlencoded {
+ form-data-key: {{form-data-key}}
+}
+
+script:pre-request {
+ bru.setVar('form-data-key', 'form-data-value');
+}
+
+assert {
+ res.body: eq form-data-key=form-data-value
+}
diff --git a/packages/bruno-tests/collection/echo/echo multipart scripting.bru b/packages/bruno-tests/collection/echo/echo multipart scripting.bru
new file mode 100644
index 000000000..13c1f2051
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/echo multipart scripting.bru
@@ -0,0 +1,22 @@
+meta {
+ name: echo multipart via scripting
+ type: http
+ seq: 10
+}
+
+post {
+ url: {{echo-host}}
+ body: multipartForm
+ auth: none
+}
+
+assert {
+ res.body: contains form-data-value
+}
+
+script:pre-request {
+ const FormData = require("form-data");
+ const form = new FormData();
+ form.append('form-data-key', 'form-data-value');
+ req.setBody(form);
+}
diff --git a/packages/bruno-tests/collection/echo/echo multipart.bru b/packages/bruno-tests/collection/echo/echo multipart.bru
new file mode 100644
index 000000000..b8fd8abf7
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/echo multipart.bru
@@ -0,0 +1,24 @@
+meta {
+ name: echo multipart
+ type: http
+ seq: 8
+}
+
+post {
+ url: {{echo-host}}
+ body: multipartForm
+ auth: none
+}
+
+body:multipart-form {
+ foo: {{form-data-key}}
+ file: @file(bruno.png)
+}
+
+assert {
+ res.body: contains form-data-value
+}
+
+script:pre-request {
+ bru.setVar('form-data-key', 'form-data-value');
+}
diff --git a/packages/bruno-tests/collection/environments/Prod.bru b/packages/bruno-tests/collection/environments/Prod.bru
index 4bea1e77a..ce8fa60cc 100644
--- a/packages/bruno-tests/collection/environments/Prod.bru
+++ b/packages/bruno-tests/collection/environments/Prod.bru
@@ -7,4 +7,5 @@ vars {
bark: {{process.env.PROC_ENV_VAR}}
foo: bar
testSetEnvVar: bruno-29653
+ echo-host: https://echo.usebruno.com
}
diff --git a/packages/bruno-tests/src/auth/index.js b/packages/bruno-tests/src/auth/index.js
index 6d6ebfb55..e26a65529 100644
--- a/packages/bruno-tests/src/auth/index.js
+++ b/packages/bruno-tests/src/auth/index.js
@@ -3,6 +3,7 @@ const router = express.Router();
const authBearer = require('./bearer');
const authBasic = require('./basic');
+const authWsse = require('./wsse');
const authCookie = require('./cookie');
const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials');
const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode');
@@ -13,6 +14,7 @@ router.use('/oauth2/authorization_code', authOAuth2AuthorizationCode);
router.use('/oauth2/client_credentials', authOAuth2ClientCredentials);
router.use('/bearer', authBearer);
router.use('/basic', authBasic);
+router.use('/wsse', authWsse);
router.use('/cookie', authCookie);
module.exports = router;
diff --git a/packages/bruno-tests/src/auth/wsse.js b/packages/bruno-tests/src/auth/wsse.js
new file mode 100644
index 000000000..1af574a3d
--- /dev/null
+++ b/packages/bruno-tests/src/auth/wsse.js
@@ -0,0 +1,70 @@
+'use strict';
+
+const express = require('express');
+const router = express.Router();
+const crypto = require('crypto');
+
+function sha256(data) {
+ return crypto.createHash('sha256').update(data).digest('base64');
+}
+
+function validateWSSE(req, res, next) {
+ const wsseHeader = req.headers['x-wsse'];
+ if (!wsseHeader) {
+ return unauthorized(res, 'WSSE header is missing');
+ }
+
+ const regex = /UsernameToken Username="(.+?)", PasswordDigest="(.+?)", (?:Nonce|nonce)="(.+?)", Created="(.+?)"/;
+ const matches = wsseHeader.match(regex);
+
+ if (!matches) {
+ return unauthorized(res, 'Invalid WSSE header format');
+ }
+
+ const [_, username, passwordDigest, nonce, created] = matches;
+ const expectedPassword = 'bruno'; // Ideally store in a config or env variable
+ const expectedDigest = sha256(nonce + created + expectedPassword);
+
+ if (passwordDigest !== expectedDigest) {
+ return unauthorized(res, 'Invalid credentials');
+ }
+
+ next();
+}
+
+// Helper to respond with an unauthorized SOAP fault
+function unauthorized(res, message) {
+ const faultResponse = `
+
+
+
+
+ soapenv:Client
+ ${message}
+
+
+
+ `;
+ res.status(401).set('Content-Type', 'text/xml');
+ res.send(faultResponse);
+}
+
+const responses = {
+ success: `
+
+
+
+
+ Success
+
+
+
+ `
+};
+
+router.post('/protected', validateWSSE, (req, res) => {
+ res.set('Content-Type', 'text/xml');
+ res.send(responses.success);
+});
+
+module.exports = router;