mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 11:51:30 +00:00
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:
@@ -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% {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "wss-custom-ca-certs-test",
|
||||
"type": "collection",
|
||||
"ignore": ["node_modules", ".git"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "wss-custom-ca-certs"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
'''
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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\"/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user