feat: add support for ssl cert in websockt (#6286)

* feat: add support for ssl cert in websockt

* improvements

* add: wss in animation

* fix: avoid a race condition between the locator's promise and the expect call

JS starts resolving promises even without the await unless it's a function, this can cause a race in this case

---------

Co-authored-by: Sid <siddharth@usebruno.com>
This commit is contained in:
Pooja
2025-12-16 17:12:47 +05:30
committed by GitHub
parent a9c63e6f2a
commit fdff792476
11 changed files with 172 additions and 10 deletions

View File

@@ -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% {

View File

@@ -180,6 +180,7 @@ const ClientCertSettings = ({ collection }) => {
<span className="protocol-placeholder">
<span className="protocol-https">https://</span>
<span className="protocol-grpcs">grpcs://</span>
<span className="protocol-wss">wss://</span>
</span>
</div>
<input

View File

@@ -65,7 +65,8 @@ const getCertsAndProxyConfig = async ({
const domain = interpolateString(clientCert?.domain, interpolationOptions);
const type = clientCert?.type || 'cert';
if (domain) {
const hostRegex = '^(https:\\/\\/|grpc:\\/\\/|grpcs:\\/\\/)?' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
const hostRegex = '^(https:\\/\\/|grpc:\\/\\/|grpcs:\\/\\/|ws:\\/\\/|wss:\\/\\/)?'
+ domain.replaceAll('.', '\\.').replaceAll('*', '.*');
const requestUrl = interpolateString(request.url, interpolationOptions);
if (requestUrl && requestUrl.match(hostRegex)) {
if (type === 'cert') {

View File

@@ -230,6 +230,29 @@ const registerWsEventHandlers = (window) => {
}
}
// 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
}
});

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -0,0 +1,6 @@
{
"version": "1",
"name": "wss-custom-ca-certs-test",
"type": "collection",
"ignore": ["node_modules", ".git"]
}

View File

@@ -0,0 +1,3 @@
{
"name": "wss-custom-ca-certs"
}

View File

@@ -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"
}
'''
}

View File

@@ -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
}
}
}
}

View File

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