Send and store cookies for websocket requests (#5205)

* Send and store cookies for websocket requests

* Lowercase for consistency
This commit is contained in:
David Marby 2022-09-22 10:48:43 +02:00 committed by GitHub
parent 5c109ac496
commit 814791f9f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 54 additions and 7 deletions

View File

@ -13,6 +13,12 @@ export function startWebSocketServer(server: Server) {
}); });
wsServer.on('connection', handleConnection); wsServer.on('connection', handleConnection);
wsServer.on('headers', (headers, request) => {
if (request.url === '/cookies') {
headers.push('Set-Cookie: insomnia-websocket-test-cookie=foo');
}
});
} }
const handleConnection = (ws: WebSocket, req: IncomingMessage) => { const handleConnection = (ws: WebSocket, req: IncomingMessage) => {

View File

@ -1,6 +1,7 @@
import electron, { ipcMain } from 'electron'; import electron, { ipcMain } from 'electron';
import fs from 'fs'; import fs from 'fs';
import { IncomingMessage } from 'http'; import { IncomingMessage } from 'http';
import { jarFromCookies } from 'insomnia-cookies';
import { setDefaultProtocol } from 'insomnia-url'; import { setDefaultProtocol } from 'insomnia-url';
import mkdirp from 'mkdirp'; import mkdirp from 'mkdirp';
import path from 'path'; import path from 'path';
@ -15,15 +16,17 @@ import {
} from 'ws'; } from 'ws';
import { AUTH_BASIC, AUTH_BEARER } from '../../common/constants'; import { AUTH_BASIC, AUTH_BEARER } from '../../common/constants';
import { generateId } from '../../common/misc'; import { generateId, getSetCookieHeaders } from '../../common/misc';
import { webSocketRequest } from '../../models'; import { webSocketRequest } from '../../models';
import * as models from '../../models'; import * as models from '../../models';
import { CookieJar } from '../../models/cookie-jar';
import { Environment } from '../../models/environment'; import { Environment } from '../../models/environment';
import { RequestAuthentication, RequestHeader } from '../../models/request'; import { RequestAuthentication, RequestHeader } from '../../models/request';
import { BaseWebSocketRequest } from '../../models/websocket-request'; import { BaseWebSocketRequest } from '../../models/websocket-request';
import type { WebSocketResponse } from '../../models/websocket-response'; import type { WebSocketResponse } from '../../models/websocket-response';
import { getBasicAuthHeader } from '../../network/basic-auth/get-header'; import { getBasicAuthHeader } from '../../network/basic-auth/get-header';
import { getBearerAuthHeader } from '../../network/bearer-auth/get-header'; import { getBearerAuthHeader } from '../../network/bearer-auth/get-header';
import { addSetCookiesToToughCookieJar } from '../../network/network';
import { urlMatchesCertHost } from '../../network/url-matches-cert-host'; import { urlMatchesCertHost } from '../../network/url-matches-cert-host';
export interface WebSocketConnection extends WebSocket { export interface WebSocketConnection extends WebSocket {
@ -97,6 +100,7 @@ const createWebSocketConnection = async (
url: string; url: string;
headers: RequestHeader[]; headers: RequestHeader[];
authentication: RequestAuthentication; authentication: RequestAuthentication;
cookieJar: CookieJar;
} }
): Promise<void> => { ): Promise<void> => {
const existingConnection = WebSocketConnections.get(options.requestId); const existingConnection = WebSocketConnections.get(options.requestId);
@ -172,6 +176,14 @@ const createWebSocketConnection = async (
} }
}); });
if (request.settingSendCookies && options.cookieJar.cookies.length) {
const jar = jarFromCookies(options.cookieJar.cookies);
const cookieHeader = jar.getCookieStringSync(options.url);
if (cookieHeader) {
lowerCasedEnabledHeaders['cookie'] = cookieHeader;
}
}
const ws = new WebSocket(options.url, { const ws = new WebSocket(options.url, {
headers: lowerCasedEnabledHeaders, headers: lowerCasedEnabledHeaders,
cert: pemCertificates, cert: pemCertificates,
@ -186,7 +198,6 @@ const createWebSocketConnection = async (
// @ts-expect-error -- private property // @ts-expect-error -- private property
const internalRequestHeader = ws._req._header; const internalRequestHeader = ws._req._header;
const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(options.url, incomingMessage, internalRequestHeader); const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(options.url, incomingMessage, internalRequestHeader);
timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
const responsePatch: Partial<WebSocketResponse> = { const responsePatch: Partial<WebSocketResponse> = {
_id: responseId, _id: responseId,
parentId: request._id, parentId: request._id,
@ -199,10 +210,30 @@ const createWebSocketConnection = async (
elapsedTime: performance.now() - start, elapsedTime: performance.now() - start,
timelinePath, timelinePath,
eventLogPath: responseBodyPath, eventLogPath: responseBodyPath,
settingSendCookies: request.settingSendCookies,
settingStoreCookies: request.settingStoreCookies,
}; };
const settings = await models.settings.getOrCreate(); const settings = await models.settings.getOrCreate();
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses); models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null }); models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null });
if (request.settingStoreCookies) {
const setCookieStrings: string[] = getSetCookieHeaders(responseHeaders).map(h => h.value);
const totalSetCookies = setCookieStrings.length;
if (totalSetCookies) {
const currentUrl = request.url;
const { cookies, rejectedCookies } = await addSetCookiesToToughCookieJar({ setCookieStrings, currentUrl, cookieJar: options.cookieJar });
rejectedCookies.forEach(errorMessage => timeline.push({ value: `Rejected cookie: ${errorMessage}`, name: 'Text', timestamp: Date.now() }));
const hasCookiesToPersist = totalSetCookies > rejectedCookies.length;
if (hasCookiesToPersist) {
await models.cookieJar.update(options.cookieJar, { cookies });
timeline.push({ value: `Saved ${totalSetCookies} cookies`, name: 'Text', timestamp: Date.now() });
}
}
}
timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
}); });
ws.on('unexpected-response', async (clientRequest, incomingMessage) => { ws.on('unexpected-response', async (clientRequest, incomingMessage) => {
incomingMessage.on('data', chunk => { incomingMessage.on('data', chunk => {
@ -224,6 +255,8 @@ const createWebSocketConnection = async (
elapsedTime: performance.now() - start, elapsedTime: performance.now() - start,
timelinePath, timelinePath,
eventLogPath: responseBodyPath, eventLogPath: responseBodyPath,
settingSendCookies: request.settingSendCookies,
settingStoreCookies: request.settingStoreCookies,
}; };
const settings = await models.settings.getOrCreate(); const settings = await models.settings.getOrCreate();
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses); models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
@ -402,6 +435,7 @@ export interface WebSocketBridgeAPI {
url: string; url: string;
headers: RequestHeader[]; headers: RequestHeader[];
authentication: RequestAuthentication; authentication: RequestAuthentication;
cookieJar: CookieJar;
}) => void; }) => void;
close: typeof closeWebSocketConnection; close: typeof closeWebSocketConnection;
closeAll: typeof closeAllWebSocketConnections; closeAll: typeof closeAllWebSocketConnections;

View File

@ -20,6 +20,8 @@ export interface BaseWebSocketRequest {
authentication: RequestAuthentication; authentication: RequestAuthentication;
parameters: RequestParameter[]; parameters: RequestParameter[];
settingEncodeUrl: boolean; settingEncodeUrl: boolean;
settingStoreCookies: boolean;
settingSendCookies: boolean;
} }
export type WebSocketRequest = BaseModel & BaseWebSocketRequest & { type: typeof type }; export type WebSocketRequest = BaseModel & BaseWebSocketRequest & { type: typeof type };
@ -40,6 +42,8 @@ export const init = (): BaseWebSocketRequest => ({
authentication: {}, authentication: {},
parameters: [], parameters: [],
settingEncodeUrl: true, settingEncodeUrl: true,
settingStoreCookies: true,
settingSendCookies: true,
}); });
export const migrate = (doc: WebSocketRequest) => doc; export const migrate = (doc: WebSocketRequest) => doc;

View File

@ -203,7 +203,7 @@ export const getCurrentUrl = ({ headerResults, finalUrl }: { headerResults: any;
} }
}; };
const addSetCookiesToToughCookieJar = async ({ setCookieStrings, currentUrl, cookieJar }: any) => { export const addSetCookiesToToughCookieJar = async ({ setCookieStrings, currentUrl, cookieJar }: any) => {
const rejectedCookies: string[] = []; const rejectedCookies: string[] = [];
const jar = jarFromCookies(cookieJar.cookies); const jar = jarFromCookies(cookieJar.cookies);
for (const setCookieStr of setCookieStrings) { for (const setCookieStr of setCookieStrings) {

View File

@ -5,6 +5,7 @@ import styled from 'styled-components';
import { hotKeyRefs } from '../../../common/hotkeys'; import { hotKeyRefs } from '../../../common/hotkeys';
import { executeHotKey } from '../../../common/hotkeys-listener'; import { executeHotKey } from '../../../common/hotkeys-listener';
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render'; import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render';
import { cookieJar } from '../../../models';
import { WebSocketRequest } from '../../../models/websocket-request'; import { WebSocketRequest } from '../../../models/websocket-request';
import { ReadyState } from '../../context/websocket-client/use-ws-ready-state'; import { ReadyState } from '../../context/websocket-client/use-ws-ready-state';
import { OneLineEditor } from '../codemirror/one-line-editor'; import { OneLineEditor } from '../codemirror/one-line-editor';
@ -81,12 +82,14 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ request, workspaceId, e
try { try {
const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND }); const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND });
const { url: rawUrl, headers, authentication, parameters } = request; const { url: rawUrl, headers, authentication, parameters } = request;
// Render any nunjucks tags in the url/headers/authentication settings // Render any nunjucks tags in the url/headers/authentication settings/cookies
const workspaceCookieJar = await cookieJar.getOrCreateForParentId(workspaceId);
const rendered = await render({ const rendered = await render({
url: rawUrl, url: rawUrl,
headers, headers,
authentication, authentication,
parameters, parameters,
workspaceCookieJar,
}, renderContext); }, renderContext);
const queryString = buildQueryStringFromParams(rendered.parameters); const queryString = buildQueryStringFromParams(rendered.parameters);
const url = joinUrlAndQueryString(rendered.url, queryString); const url = joinUrlAndQueryString(rendered.url, queryString);
@ -96,6 +99,7 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ request, workspaceId, e
url, url,
headers: rendered.headers, headers: rendered.headers,
authentication: rendered.authentication, authentication: rendered.authentication,
cookieJar: rendered.workspaceCookieJar,
}); });
} catch (err) { } catch (err) {
if (err.type === 'render') { if (err.type === 'render') {

View File

@ -180,9 +180,8 @@ const WebSocketActiveResponsePane: FC<{ requestId: string; response: WebSocketRe
<div className="scrollable pad"> <div className="scrollable pad">
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center"> <ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
<ResponseCookiesViewer <ResponseCookiesViewer
// @TODO: Implement cookie storing and sending cookiesSent={response.settingSendCookies}
cookiesSent={false} cookiesStored={response.settingStoreCookies}
cookiesStored={false}
headers={cookieHeaders} headers={cookieHeaders}
/> />
</ErrorBoundary> </ErrorBoundary>