Skip to content

Commit da48871

Browse files
marckongjackkav
authored andcommitted
[INS-1833] Include Auth Header in Headers mapping for WebSocket Connection (#5120)
* add auth to the header * remove console log * remove unneeded async * add success redirect logic to websocket server * add unexpected-response handler Co-authored-by: jackkav <[email protected]>
1 parent ba7835b commit da48871

File tree

2 files changed

+134
-24
lines changed

2 files changed

+134
-24
lines changed

packages/insomnia-smoke-test/server/websocket.ts

+39-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { IncomingMessage, Server } from 'http';
2+
import { Socket } from 'net';
23
import { WebSocket, WebSocketServer } from 'ws';
34

45
/**
56
* Starts an echo WebSocket server that receives messages from a client and echoes them back.
67
*/
78
export function startWebSocketServer(server: Server, httpsServer: Server) {
8-
const wsServer = new WebSocketServer({ server });
9-
const wssServer = new WebSocketServer({ server: httpsServer });
9+
const wsServer = new WebSocketServer({ noServer: true });
10+
const wssServer = new WebSocketServer({ noServer: true });
1011

12+
server.on('upgrade', (request, socket, head) => {
13+
upgrade(wsServer, request, socket, head);
14+
});
15+
httpsServer.on('upgrade', (request, socket, head) => {
16+
upgrade(wssServer, request, socket, head);
17+
});
1118
wsServer.on('connection', handleConnection);
1219
wssServer.on('connection', handleConnection);
1320
}
@@ -31,3 +38,33 @@ const handleConnection = (ws: WebSocket, req: IncomingMessage) => {
3138
console.log('WebSocket connection was closed');
3239
});
3340
};
41+
const redirectOnSuccess = (socket: Socket) => {
42+
socket.end(`HTTP/1.1 302 Found
43+
Location: ws://localhost:4010
44+
45+
`);
46+
return;
47+
};
48+
const upgrade = (wss: WebSocketServer, request: IncomingMessage, socket: Socket, head: Buffer) => {
49+
if (request.url === '/redirect') {
50+
return redirectOnSuccess(socket);
51+
}
52+
if (request.url === '/bearer') {
53+
if (request.headers.authorization !== 'Bearer insomnia-cool-token-!!!1112113243111') {
54+
socket.end('HTTP/1.1 401 Unauthorized\n\n');
55+
return;
56+
}
57+
return redirectOnSuccess(socket);
58+
}
59+
if (request.url === '/basic-auth') {
60+
// login with user:password
61+
if (request.headers.authorization !== 'Basic dXNlcjpwYXNzd29yZA==') {
62+
socket.end('HTTP/1.1 401 Unauthorized\n\n');
63+
return;
64+
}
65+
return redirectOnSuccess(socket);
66+
}
67+
wss.handleUpgrade(request, socket, head, ws => {
68+
wss.emit('connection', ws, request);
69+
});
70+
};

packages/insomnia/src/main/network/websocket.ts

+95-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import electron, { ipcMain } from 'electron';
22
import fs from 'fs';
3+
import { IncomingMessage } from 'http';
34
import { setDefaultProtocol } from 'insomnia-url';
45
import mkdirp from 'mkdirp';
56
import path from 'path';
@@ -13,11 +14,15 @@ import {
1314
WebSocket,
1415
} from 'ws';
1516

17+
import { AUTH_BASIC, AUTH_BEARER } from '../../common/constants';
1618
import { generateId } from '../../common/misc';
1719
import { websocketRequest } from '../../models';
1820
import * as models from '../../models';
21+
import { RequestAuthentication, RequestHeader } from '../../models/request';
1922
import type { Response } from '../../models/response';
2023
import { BaseWebSocketRequest } from '../../models/websocket-request';
24+
import { getBasicAuthHeader } from '../../network/basic-auth/get-header';
25+
import { getBearerAuthHeader } from '../../network/bearer-auth/get-header';
2126
import { urlMatchesCertHost } from '../../network/url-matches-cert-host';
2227

2328
export interface WebSocketConnection extends WebSocket {
@@ -96,6 +101,23 @@ function dispatchWebSocketEvent(target: Electron.WebContents, eventChannel: stri
96101
}
97102
}
98103

104+
const parseResponseAndBuildTimeline = (url: string, incomingMessage: IncomingMessage, clientRequestHeaders: string) => {
105+
const statusMessage = incomingMessage.statusMessage || '';
106+
const statusCode = incomingMessage.statusCode || 0;
107+
const httpVersion = incomingMessage.httpVersion;
108+
const responseHeaders = Object.entries(incomingMessage.headers).map(([name, value]) => ({ name, value: value?.toString() || '' }));
109+
const headersIn = responseHeaders.map(({ name, value }) => `${name}: ${value}`).join('\n');
110+
const timeline = [
111+
{ value: `Preparing request to ${url}`, name: 'Text', timestamp: Date.now() },
112+
{ value: `Current time is ${new Date().toISOString()}`, name: 'Text', timestamp: Date.now() },
113+
{ value: 'Using HTTP 1.1', name: 'Text', timestamp: Date.now() },
114+
{ value: clientRequestHeaders, name: 'HeaderOut', timestamp: Date.now() },
115+
{ value: `HTTP/${httpVersion} ${statusCode} ${statusMessage}`, name: 'HeaderIn', timestamp: Date.now() },
116+
{ value: headersIn, name: 'HeaderIn', timestamp: Date.now() },
117+
];
118+
return { timeline, responseHeaders, statusCode, statusMessage, httpVersion };
119+
};
120+
99121
async function createWebSocketConnection(
100122
event: Electron.IpcMainInvokeEvent,
101123
options: { requestId: string; workspaceId: string }
@@ -122,11 +144,24 @@ async function createWebSocketConnection(
122144
try {
123145
const eventChannel = `webSocketRequest.connection.${responseId}.event`;
124146
const readyStateChannel = `webSocketRequest.connection.${request._id}.readyState`;
125-
126147
// @TODO: Render nunjucks tags in these headers
127148
const reduceArrayToLowerCaseKeyedDictionary = (acc: { [key: string]: string }, { name, value }: BaseWebSocketRequest['headers'][0]) =>
128149
({ ...acc, [name.toLowerCase() || '']: value || '' });
129-
const headers = request.headers.filter(({ value, disabled }) => !!value && !disabled)
150+
const headers = request.headers;
151+
if (request.authentication.disabled === false) {
152+
if (request.authentication.type === AUTH_BASIC) {
153+
const { username, password, useISO88591 } = request.authentication;
154+
const encoding = useISO88591 ? 'latin1' : 'utf8';
155+
headers.push(getBasicAuthHeader(username, password, encoding));
156+
}
157+
if (request.authentication.type === AUTH_BEARER) {
158+
const { token, prefix } = request.authentication;
159+
headers.push(getBearerAuthHeader(token, prefix));
160+
}
161+
}
162+
163+
const lowerCasedEnabledHeaders = headers
164+
.filter(({ value, disabled }) => !!value && !disabled)
130165
.reduce(reduceArrayToLowerCaseKeyedDictionary, {});
131166

132167
const settings = await models.settings.getOrCreate();
@@ -158,34 +193,43 @@ async function createWebSocketConnection(
158193
});
159194

160195
const ws = new WebSocket(request.url, {
161-
headers,
196+
headers: lowerCasedEnabledHeaders,
162197
cert: pemCertificates,
163198
key: pemCertificateKeys,
164199
pfx: pfxCertificates,
165200
rejectUnauthorized: settings.validateSSL,
201+
followRedirects: true,
166202
});
167203
WebSocketConnections.set(options.requestId, ws);
168204

169-
ws.on('upgrade', async incoming => {
205+
ws.on('upgrade', async incomingMessage => {
170206
// @ts-expect-error -- private property
171-
const internalRequest = ws._req;
172-
// response
173-
const statusMessage = incoming.statusMessage || '';
174-
const statusCode = incoming.statusCode || 0;
175-
const httpVersion = incoming.httpVersion;
176-
const responseHeaders = Object.entries(incoming.headers).map(([name, value]) => ({ name, value: value?.toString() || '' }));
177-
const headersIn = responseHeaders.map(({ name, value }) => `${name}: ${value}`).join('\n');
178-
179-
// @TODO: We may want to add set-cookie handling here.
180-
[
181-
{ value: `Preparing request to ${request.url}`, name: 'Text', timestamp: Date.now() },
182-
{ value: `Current time is ${new Date().toISOString()}`, name: 'Text', timestamp: Date.now() },
183-
{ value: 'Using HTTP 1.1', name: 'Text', timestamp: Date.now() },
184-
{ value: internalRequest._header, name: 'HeaderOut', timestamp: Date.now() },
185-
{ value: `HTTP/${httpVersion} ${statusCode} ${statusMessage}`, name: 'HeaderIn', timestamp: Date.now() },
186-
{ value: headersIn, name: 'HeaderIn', timestamp: Date.now() },
187-
].map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
188-
207+
const internalRequestHeader = ws._req._header;
208+
const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(request.url, incomingMessage, internalRequestHeader);
209+
timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
210+
const responsePatch: Partial<Response> = {
211+
_id: responseId,
212+
parentId: request._id,
213+
headers: responseHeaders,
214+
url: request.url,
215+
statusCode,
216+
statusMessage,
217+
httpVersion,
218+
elapsedTime: performance.now() - start,
219+
timelinePath,
220+
bodyPath: responseBodyPath,
221+
// NOTE: required for legacy zip workaround
222+
bodyCompression: null,
223+
};
224+
const settings = await models.settings.getOrCreate();
225+
models.response.create(responsePatch, settings.maxHistoryResponses);
226+
models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null });
227+
});
228+
ws.on('unexpected-response', async (clientRequest, incomingMessage) => {
229+
// @ts-expect-error -- private property
230+
const internalRequestHeader = clientRequest._header;
231+
const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(request.url, incomingMessage, internalRequestHeader);
232+
timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
189233
const responsePatch: Partial<Response> = {
190234
_id: responseId,
191235
parentId: request._id,
@@ -203,6 +247,7 @@ async function createWebSocketConnection(
203247
const settings = await models.settings.getOrCreate();
204248
models.response.create(responsePatch, settings.maxHistoryResponses);
205249
models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null });
250+
deleteRequestMaps(request._id, `Unexpected response ${incomingMessage.statusCode}`);
206251
});
207252

208253
ws.addEventListener('open', () => {
@@ -413,3 +458,31 @@ electron.app.on('window-all-closed', () => {
413458
ws.close();
414459
});
415460
});
461+
462+
export function getAuthHeader(authentication: RequestAuthentication): RequestHeader | undefined {
463+
if (!authentication || authentication.disabled) {
464+
return;
465+
}
466+
467+
switch (authentication.type) {
468+
case 'basic': {
469+
const { username, password, useISO88591 } = authentication;
470+
const encoding = useISO88591 ? 'latin1' : 'utf8';
471+
const header = getBasicAuthHeader(username, password, encoding);
472+
return header;
473+
}
474+
475+
case 'bearer': {
476+
const { token, prefix } = authentication;
477+
return getBearerAuthHeader(token, prefix);
478+
}
479+
480+
case 'digest': {
481+
return;
482+
}
483+
484+
default: {
485+
return;
486+
}
487+
}
488+
}

0 commit comments

Comments
 (0)