diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js
index affe1f7db..34773ac0d 100644
--- a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js
@@ -48,32 +48,37 @@ const StyledWrapper = styled.div`
}
.protocol-https,
- .protocol-grpcs {
+ .protocol-grpcs,
+ .protocol-wss {
position: absolute;
right: 8px;
top: 0;
bottom: 0;
- transition: transform 0.3s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
}
.protocol-https {
- animation: slideUpDown 6s infinite;
+ animation: slideUpDown 9s infinite;
transform: translateY(0);
}
.protocol-grpcs {
- animation: slideUpDown 6s infinite 3s;
+ animation: slideUpDown 9s infinite 3s;
+ transform: translateY(100%);
+ }
+
+ .protocol-wss {
+ animation: slideUpDown 9s infinite 6s;
transform: translateY(100%);
}
@keyframes slideUpDown {
- 0%, 45% {
+ 0%, 30% {
transform: translateY(0);
}
- 50%, 95% {
+ 33.33%, 97% {
transform: translateY(100%);
}
100% {
diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js
index 2b13f529e..e9a16e666 100644
--- a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js
@@ -180,6 +180,7 @@ const ClientCertSettings = ({ collection }) => {
https://
grpcs://
+ wss://
{
}
}
+ // Get certificates and proxy configuration
+ const certsAndProxyConfig = await getCertsAndProxyConfig({
+ collectionUid: collection.uid,
+ collection,
+ request: requestCopy.request,
+ envVars: preparedRequest.envVars,
+ runtimeVariables,
+ processEnvVars: preparedRequest.processEnvVars,
+ collectionPath: collection.pathname,
+ globalEnvironmentVariables: collection.globalEnvironmentVariables
+ });
+
+ const { httpsAgentRequestFields } = certsAndProxyConfig;
+
+ const sslOptions = {
+ rejectUnauthorized: preferencesUtil.shouldVerifyTls(),
+ ca: httpsAgentRequestFields.ca,
+ cert: httpsAgentRequestFields.cert,
+ key: httpsAgentRequestFields.key,
+ pfx: httpsAgentRequestFields.pfx,
+ passphrase: httpsAgentRequestFields.passphrase
+ };
+
// Start WebSocket connection
await wsClient.startConnection({
request: preparedRequest,
@@ -237,7 +260,8 @@ const registerWsEventHandlers = (window) => {
options: {
timeout: settings.timeout,
keepAlive: settings.keepAliveInterval > 0 ? true : false,
- keepAliveInterval: settings.keepAliveInterval
+ keepAliveInterval: settings.keepAliveInterval,
+ sslOptions
}
});
diff --git a/packages/bruno-requests/src/ws/ws-client.js b/packages/bruno-requests/src/ws/ws-client.js
index cb6fee6d7..a8fb45f1b 100644
--- a/packages/bruno-requests/src/ws/ws-client.js
+++ b/packages/bruno-requests/src/ws/ws-client.js
@@ -40,7 +40,7 @@ class WsClient {
*/
async startConnection({ request, collection, options = {} }) {
const { url, headers } = request;
- const { timeout = 30000, keepAlive = false, keepAliveInterval = 10_000 } = options;
+ const { timeout = 30000, keepAlive = false, keepAliveInterval = 10_000, sslOptions = {} } = options;
const parsedUrl = getParsedWsUrlObject(url);
const timeoutAsNumber = Number(timeout);
@@ -63,7 +63,13 @@ class WsClient {
const wsOptions = {
headers,
handshakeTimeout: validTimeout,
- followRedirects: true
+ followRedirects: true,
+ rejectUnauthorized: sslOptions.rejectUnauthorized,
+ ca: sslOptions.ca,
+ cert: sslOptions.cert,
+ key: sslOptions.key,
+ pfx: sslOptions.pfx,
+ passphrase: sslOptions.passphrase
};
if (protocolVersion) {
diff --git a/tests/ssl/custom-ca-certs/server/index.js b/tests/ssl/custom-ca-certs/server/index.js
index 7d0725e83..b92b31513 100644
--- a/tests/ssl/custom-ca-certs/server/index.js
+++ b/tests/ssl/custom-ca-certs/server/index.js
@@ -3,6 +3,7 @@
const path = require('node:path');
const fs = require('node:fs');
const https = require('node:https');
+const WebSocket = require('ws');
const { killProcessOnPort } = require('./helpers/platform');
function createServer(certsDir, port = 8090) {
@@ -17,6 +18,56 @@ function createServer(certsDir, port = 8090) {
res.end('helloworld');
});
+ // Create WebSocket server for WSS support
+ const wss = new WebSocket.Server({ noServer: true });
+
+ wss.on('connection', function connection(ws, request) {
+ ws.on('error', function error(err) {
+ console.error('WebSocket error:', err.message);
+ });
+
+ ws.on('message', function message(data) {
+ const msg = Buffer.from(data).toString().trim();
+ let isJSON = false;
+ let obj = {};
+ try {
+ obj = JSON.parse(msg);
+ isJSON = true;
+ } catch (err) {
+ // Not a JSON value
+ }
+ if (isJSON) {
+ if ('func' in obj && obj.func === 'headers') {
+ return ws.send(JSON.stringify({
+ headers: request.headers
+ }));
+ } else if ('func' in obj && obj.func === 'query') {
+ const url = new URL(request.url, `https://${request.headers.host}`);
+ const query = Object.fromEntries(url.searchParams.entries());
+ return ws.send(JSON.stringify({
+ query: query
+ }));
+ } else {
+ return ws.send(JSON.stringify({
+ data: obj
+ }));
+ }
+ }
+ return ws.send(Buffer.from(data).toString());
+ });
+ });
+
+ // Handle WebSocket upgrade requests
+ server.on('upgrade', (request, socket, head) => {
+ if (request.url.startsWith('/ws')) {
+ wss.handleUpgrade(request, socket, head, (ws) => {
+ wss.emit('connection', ws, request);
+ });
+ } else {
+ socket.destroy();
+ }
+ });
+
return new Promise((resolve, reject) => {
server.listen(port, (error) => {
if (error) {
diff --git a/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/bruno.json b/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/bruno.json
new file mode 100644
index 000000000..d6aaa8cda
--- /dev/null
+++ b/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/bruno.json
@@ -0,0 +1,6 @@
+{
+ "version": "1",
+ "name": "wss-custom-ca-certs-test",
+ "type": "collection",
+ "ignore": ["node_modules", ".git"]
+}
diff --git a/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/package.json b/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/package.json
new file mode 100644
index 000000000..5484b758f
--- /dev/null
+++ b/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/package.json
@@ -0,0 +1,3 @@
+{
+ "name": "wss-custom-ca-certs"
+}
diff --git a/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/ws-ssl-request.bru b/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/ws-ssl-request.bru
new file mode 100644
index 000000000..690f22b4b
--- /dev/null
+++ b/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/ws-ssl-request.bru
@@ -0,0 +1,19 @@
+meta {
+ name: ws-ssl-request
+ type: ws
+ seq: 1
+}
+
+ws {
+ url: wss://localhost:8090/ws
+ auth: inherit
+}
+
+body:ws {
+ name: message 1
+ content: '''
+ {
+ "func":"headers"
+ }
+ '''
+}
diff --git a/tests/ssl/custom-ca-certs/tests/wss-success/init-user-data/preferences.json b/tests/ssl/custom-ca-certs/tests/wss-success/init-user-data/preferences.json
new file mode 100644
index 000000000..5a1df08ed
--- /dev/null
+++ b/tests/ssl/custom-ca-certs/tests/wss-success/init-user-data/preferences.json
@@ -0,0 +1,16 @@
+{
+ "maximized": true,
+ "lastOpenedCollections": ["{{projectRoot}}/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection"],
+ "preferences": {
+ "request": {
+ "sslVerification": true,
+ "customCaCertificate": {
+ "enabled": true,
+ "filePath": "{{projectRoot}}/tests/ssl/custom-ca-certs/server/certs/ca-cert.pem"
+ },
+ "keepDefaultCaCertificates": {
+ "enabled": false
+ }
+ }
+ }
+}
diff --git a/tests/ssl/custom-ca-certs/tests/wss-success/wss-success.spec.ts b/tests/ssl/custom-ca-certs/tests/wss-success/wss-success.spec.ts
new file mode 100644
index 000000000..5a11900a2
--- /dev/null
+++ b/tests/ssl/custom-ca-certs/tests/wss-success/wss-success.spec.ts
@@ -0,0 +1,30 @@
+import { test, expect } from '../../../../../playwright';
+import { openCollectionAndAcceptSandbox } from '../../../../utils/page';
+import { buildWebsocketCommonLocators } from '../../../../utils/page/locators';
+
+const BRU_REQ_NAME = /^ws-ssl-request$/;
+
+test.describe.serial('wss with custom ca cert', () => {
+ test('websocket connects over ssl', async ({ pageWithUserData: page }) => {
+ const locators = buildWebsocketCommonLocators(page);
+
+ // Define reusable locators
+ const requestItem = page.getByTitle(BRU_REQ_NAME);
+
+ await test.step('Open collection', async () => {
+ await openCollectionAndAcceptSandbox(page, 'wss-custom-ca-certs-test', 'safe');
+ });
+
+ await test.step('Connect to WSS', async () => {
+ await requestItem.click();
+ await locators.connectionControls.connect().click();
+ await expect(locators.connectionControls.disconnect()).toBeAttached();
+ });
+
+ await test.step('Send message and verify response', async () => {
+ await locators.runner().click();
+ const responseMessage = locators.messages().nth(2).locator('.text-ellipsis');
+ await expect(responseMessage).toHaveText(/\"headers\"/);
+ });
+ });
+});