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\"/); + }); + }); +});