From 48c870f76d8cce6a03a4f0555cf5171c0eeeb71b Mon Sep 17 00:00:00 2001 From: Mark Kim Date: Tue, 30 Aug 2022 17:10:16 -0400 Subject: [PATCH 1/5] add auth to the header --- .../insomnia/src/main/network/websocket.ts | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/insomnia/src/main/network/websocket.ts b/packages/insomnia/src/main/network/websocket.ts index 3846f08086a..e2a21c4fe7c 100644 --- a/packages/insomnia/src/main/network/websocket.ts +++ b/packages/insomnia/src/main/network/websocket.ts @@ -16,8 +16,11 @@ import { import { generateId } from '../../common/misc'; import { websocketRequest } from '../../models'; import * as models from '../../models'; +import { RequestAuthentication } from '../../models/request'; import type { Response } from '../../models/response'; import { BaseWebSocketRequest } from '../../models/websocket-request'; +import { getBasicAuthHeader } from '../../network/basic-auth/get-header'; +import { getBearerAuthHeader } from '../../network/bearer-auth/get-header'; import { urlMatchesCertHost } from '../../network/url-matches-cert-host'; export interface WebSocketConnection extends WebSocket { @@ -122,11 +125,11 @@ async function createWebSocketConnection( try { const eventChannel = `webSocketRequest.connection.${responseId}.event`; const readyStateChannel = `webSocketRequest.connection.${request._id}.readyState`; - + const authHeader = await getAuthHeader(request.authentication); // @TODO: Render nunjucks tags in these headers const reduceArrayToLowerCaseKeyedDictionary = (acc: { [key: string]: string }, { name, value }: BaseWebSocketRequest['headers'][0]) => ({ ...acc, [name.toLowerCase() || '']: value || '' }); - const headers = request.headers.filter(({ value, disabled }) => !!value && !disabled) + const headers = request.headers.concat(authHeader ?? []).filter(({ value, disabled }) => !!value && !disabled) .reduce(reduceArrayToLowerCaseKeyedDictionary, {}); const settings = await models.settings.getOrCreate(); @@ -412,3 +415,33 @@ electron.app.on('window-all-closed', () => { ws.close(); }); }); + +export async function getAuthHeader(authentication: RequestAuthentication) { + if (!authentication || authentication.disabled) { + return; + } + + switch (authentication.type) { + case 'basic': { + const { username, password, useISO88591 } = authentication; + const encoding = useISO88591 ? 'latin1' : 'utf8'; + console.log('?????????/'); + const header = getBasicAuthHeader(username, password, encoding); + console.log('header', header); + return header; + } + + case 'bearer': { + const { token, prefix } = authentication; + return getBearerAuthHeader(token, prefix); + } + + case 'digest': { + return; + } + + default: { + return; + } + } +} From c8ca0429a5bf722337c28dd2bdb0411ef08aa2fa Mon Sep 17 00:00:00 2001 From: Mark Kim Date: Tue, 30 Aug 2022 17:11:47 -0400 Subject: [PATCH 2/5] remove console log --- packages/insomnia/src/main/network/websocket.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/insomnia/src/main/network/websocket.ts b/packages/insomnia/src/main/network/websocket.ts index e2a21c4fe7c..dcc20218231 100644 --- a/packages/insomnia/src/main/network/websocket.ts +++ b/packages/insomnia/src/main/network/websocket.ts @@ -425,9 +425,7 @@ export async function getAuthHeader(authentication: RequestAuthentication) { case 'basic': { const { username, password, useISO88591 } = authentication; const encoding = useISO88591 ? 'latin1' : 'utf8'; - console.log('?????????/'); const header = getBasicAuthHeader(username, password, encoding); - console.log('header', header); return header; } From 871bc513bbc0e740d4833d1be05bf51cb380b54d Mon Sep 17 00:00:00 2001 From: Mark Kim Date: Tue, 30 Aug 2022 22:33:55 -0400 Subject: [PATCH 3/5] remove unneeded async --- packages/insomnia/src/main/network/websocket.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/insomnia/src/main/network/websocket.ts b/packages/insomnia/src/main/network/websocket.ts index dcc20218231..3cbeb6f6012 100644 --- a/packages/insomnia/src/main/network/websocket.ts +++ b/packages/insomnia/src/main/network/websocket.ts @@ -16,7 +16,7 @@ import { import { generateId } from '../../common/misc'; import { websocketRequest } from '../../models'; import * as models from '../../models'; -import { RequestAuthentication } from '../../models/request'; +import { RequestAuthentication, RequestHeader } from '../../models/request'; import type { Response } from '../../models/response'; import { BaseWebSocketRequest } from '../../models/websocket-request'; import { getBasicAuthHeader } from '../../network/basic-auth/get-header'; @@ -125,7 +125,7 @@ async function createWebSocketConnection( try { const eventChannel = `webSocketRequest.connection.${responseId}.event`; const readyStateChannel = `webSocketRequest.connection.${request._id}.readyState`; - const authHeader = await getAuthHeader(request.authentication); + const authHeader = getAuthHeader(request.authentication); // @TODO: Render nunjucks tags in these headers const reduceArrayToLowerCaseKeyedDictionary = (acc: { [key: string]: string }, { name, value }: BaseWebSocketRequest['headers'][0]) => ({ ...acc, [name.toLowerCase() || '']: value || '' }); @@ -416,7 +416,7 @@ electron.app.on('window-all-closed', () => { }); }); -export async function getAuthHeader(authentication: RequestAuthentication) { +export function getAuthHeader(authentication: RequestAuthentication): RequestHeader | undefined { if (!authentication || authentication.disabled) { return; } From 48dc8f4d3def386ea89dd06e80a12257df7ceeca Mon Sep 17 00:00:00 2001 From: jackkav Date: Thu, 1 Sep 2022 15:36:39 +0200 Subject: [PATCH 4/5] add success redirect logic to websocket server --- .../insomnia-smoke-test/server/websocket.ts | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/insomnia-smoke-test/server/websocket.ts b/packages/insomnia-smoke-test/server/websocket.ts index 789232c776f..edbdecfd2de 100644 --- a/packages/insomnia-smoke-test/server/websocket.ts +++ b/packages/insomnia-smoke-test/server/websocket.ts @@ -1,13 +1,20 @@ import { IncomingMessage, Server } from 'http'; +import { Socket } from 'net'; import { WebSocket, WebSocketServer } from 'ws'; /** * Starts an echo WebSocket server that receives messages from a client and echoes them back. */ export function startWebSocketServer(server: Server, httpsServer: Server) { - const wsServer = new WebSocketServer({ server }); - const wssServer = new WebSocketServer({ server: httpsServer }); + const wsServer = new WebSocketServer({ noServer: true }); + const wssServer = new WebSocketServer({ noServer: true }); + server.on('upgrade', (request, socket, head) => { + upgrade(wsServer, request, socket, head); + }); + httpsServer.on('upgrade', (request, socket, head) => { + upgrade(wssServer, request, socket, head); + }); wsServer.on('connection', handleConnection); wssServer.on('connection', handleConnection); } @@ -31,3 +38,33 @@ const handleConnection = (ws: WebSocket, req: IncomingMessage) => { console.log('WebSocket connection was closed'); }); }; +const redirectOnSuccess = (socket: Socket) => { + socket.end(`HTTP/1.1 302 Found +Location: ws://localhost:4010 + +`); + return; +}; +const upgrade = (wss: WebSocketServer, request: IncomingMessage, socket: Socket, head: Buffer) => { + if (request.url === '/redirect') { + return redirectOnSuccess(socket); + } + if (request.url === '/bearer') { + if (request.headers.authorization !== 'Bearer insomnia-cool-token-!!!1112113243111') { + socket.end('HTTP/1.1 401 Unauthorized\n\n'); + return; + } + return redirectOnSuccess(socket); + } + if (request.url === '/basic-auth') { + // login with user:password + if (request.headers.authorization !== 'Basic dXNlcjpwYXNzd29yZA==') { + socket.end('HTTP/1.1 401 Unauthorized\n\n'); + return; + } + return redirectOnSuccess(socket); + } + wss.handleUpgrade(request, socket, head, ws => { + wss.emit('connection', ws, request); + }); +}; From 3f64a44a36a2765980a539eb8e89d0cf8ffde8ac Mon Sep 17 00:00:00 2001 From: jackkav Date: Thu, 1 Sep 2022 15:55:44 +0200 Subject: [PATCH 5/5] add unexpected-response handler --- .../insomnia/src/main/network/websocket.ts | 87 ++++++++++++++----- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/packages/insomnia/src/main/network/websocket.ts b/packages/insomnia/src/main/network/websocket.ts index 3cbeb6f6012..0bca1d3c163 100644 --- a/packages/insomnia/src/main/network/websocket.ts +++ b/packages/insomnia/src/main/network/websocket.ts @@ -1,5 +1,6 @@ import electron, { ipcMain } from 'electron'; import fs from 'fs'; +import { IncomingMessage } from 'http'; import { setDefaultProtocol } from 'insomnia-url'; import mkdirp from 'mkdirp'; import path from 'path'; @@ -13,6 +14,7 @@ import { WebSocket, } from 'ws'; +import { AUTH_BASIC, AUTH_BEARER } from '../../common/constants'; import { generateId } from '../../common/misc'; import { websocketRequest } from '../../models'; import * as models from '../../models'; @@ -99,6 +101,23 @@ function dispatchWebSocketEvent(target: Electron.WebContents, eventChannel: stri } } +const parseResponseAndBuildTimeline = (url: string, incomingMessage: IncomingMessage, clientRequestHeaders: string) => { + const statusMessage = incomingMessage.statusMessage || ''; + const statusCode = incomingMessage.statusCode || 0; + const httpVersion = incomingMessage.httpVersion; + const responseHeaders = Object.entries(incomingMessage.headers).map(([name, value]) => ({ name, value: value?.toString() || '' })); + const headersIn = responseHeaders.map(({ name, value }) => `${name}: ${value}`).join('\n'); + const timeline = [ + { value: `Preparing request to ${url}`, name: 'Text', timestamp: Date.now() }, + { value: `Current time is ${new Date().toISOString()}`, name: 'Text', timestamp: Date.now() }, + { value: 'Using HTTP 1.1', name: 'Text', timestamp: Date.now() }, + { value: clientRequestHeaders, name: 'HeaderOut', timestamp: Date.now() }, + { value: `HTTP/${httpVersion} ${statusCode} ${statusMessage}`, name: 'HeaderIn', timestamp: Date.now() }, + { value: headersIn, name: 'HeaderIn', timestamp: Date.now() }, + ]; + return { timeline, responseHeaders, statusCode, statusMessage, httpVersion }; +}; + async function createWebSocketConnection( event: Electron.IpcMainInvokeEvent, options: { requestId: string; workspaceId: string } @@ -125,11 +144,24 @@ async function createWebSocketConnection( try { const eventChannel = `webSocketRequest.connection.${responseId}.event`; const readyStateChannel = `webSocketRequest.connection.${request._id}.readyState`; - const authHeader = getAuthHeader(request.authentication); // @TODO: Render nunjucks tags in these headers const reduceArrayToLowerCaseKeyedDictionary = (acc: { [key: string]: string }, { name, value }: BaseWebSocketRequest['headers'][0]) => ({ ...acc, [name.toLowerCase() || '']: value || '' }); - const headers = request.headers.concat(authHeader ?? []).filter(({ value, disabled }) => !!value && !disabled) + const headers = request.headers; + if (request.authentication.disabled === false) { + if (request.authentication.type === AUTH_BASIC) { + const { username, password, useISO88591 } = request.authentication; + const encoding = useISO88591 ? 'latin1' : 'utf8'; + headers.push(getBasicAuthHeader(username, password, encoding)); + } + if (request.authentication.type === AUTH_BEARER) { + const { token, prefix } = request.authentication; + headers.push(getBearerAuthHeader(token, prefix)); + } + } + + const lowerCasedEnabledHeaders = headers + .filter(({ value, disabled }) => !!value && !disabled) .reduce(reduceArrayToLowerCaseKeyedDictionary, {}); const settings = await models.settings.getOrCreate(); @@ -161,34 +193,43 @@ async function createWebSocketConnection( }); const ws = new WebSocket(request.url, { - headers, + headers: lowerCasedEnabledHeaders, cert: pemCertificates, key: pemCertificateKeys, pfx: pfxCertificates, rejectUnauthorized: settings.validateSSL, + followRedirects: true, }); WebSocketConnections.set(options.requestId, ws); - ws.on('upgrade', async incoming => { + ws.on('upgrade', async incomingMessage => { // @ts-expect-error -- private property - const internalRequest = ws._req; - // response - const statusMessage = incoming.statusMessage || ''; - const statusCode = incoming.statusCode || 0; - const httpVersion = incoming.httpVersion; - const responseHeaders = Object.entries(incoming.headers).map(([name, value]) => ({ name, value: value?.toString() || '' })); - const headersIn = responseHeaders.map(({ name, value }) => `${name}: ${value}`).join('\n'); - - // @TODO: We may want to add set-cookie handling here. - [ - { value: `Preparing request to ${request.url}`, name: 'Text', timestamp: Date.now() }, - { value: `Current time is ${new Date().toISOString()}`, name: 'Text', timestamp: Date.now() }, - { value: 'Using HTTP 1.1', name: 'Text', timestamp: Date.now() }, - { value: internalRequest._header, name: 'HeaderOut', timestamp: Date.now() }, - { value: `HTTP/${httpVersion} ${statusCode} ${statusMessage}`, name: 'HeaderIn', timestamp: Date.now() }, - { value: headersIn, name: 'HeaderIn', timestamp: Date.now() }, - ].map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n')); - + const internalRequestHeader = ws._req._header; + const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(request.url, incomingMessage, internalRequestHeader); + timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n')); + const responsePatch: Partial = { + _id: responseId, + parentId: request._id, + headers: responseHeaders, + url: request.url, + statusCode, + statusMessage, + httpVersion, + elapsedTime: performance.now() - start, + timelinePath, + bodyPath: responseBodyPath, + // NOTE: required for legacy zip workaround + bodyCompression: null, + }; + const settings = await models.settings.getOrCreate(); + models.response.create(responsePatch, settings.maxHistoryResponses); + models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null }); + }); + ws.on('unexpected-response', async (clientRequest, incomingMessage) => { + // @ts-expect-error -- private property + const internalRequestHeader = clientRequest._header; + const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(request.url, incomingMessage, internalRequestHeader); + timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n')); const responsePatch: Partial = { _id: responseId, parentId: request._id, @@ -200,11 +241,13 @@ async function createWebSocketConnection( elapsedTime: performance.now() - start, timelinePath, bodyPath: responseBodyPath, + // NOTE: required for legacy zip workaround bodyCompression: null, }; const settings = await models.settings.getOrCreate(); models.response.create(responsePatch, settings.maxHistoryResponses); models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null }); + deleteRequestMaps(request._id, `Unexpected response ${incomingMessage.statusCode}`); }); ws.addEventListener('open', () => {