diff --git a/packages/insomnia/src/common/render.ts b/packages/insomnia/src/common/render.ts index 30a084016..0d07913d2 100644 --- a/packages/insomnia/src/common/render.ts +++ b/packages/insomnia/src/common/render.ts @@ -450,6 +450,10 @@ export async function getRenderedGrpcRequestMessage( } type RenderRequestOptions = BaseRenderContextOptions & RenderRequest; +export interface RequestAndContext { + request: RenderedRequest; + context: Record; +} export async function getRenderedRequestAndContext( { request, @@ -457,7 +461,7 @@ export async function getRenderedRequestAndContext( extraInfo, purpose, }: RenderRequestOptions, -) { +): Promise { const ancestors = await getRenderContextAncestors(request); const workspace = ancestors.find(isWorkspace); const parentId = workspace ? workspace._id : 'n/a'; diff --git a/packages/insomnia/src/common/send-request.ts b/packages/insomnia/src/common/send-request.ts index cff79a37f..e7f479c63 100644 --- a/packages/insomnia/src/common/send-request.ts +++ b/packages/insomnia/src/common/send-request.ts @@ -2,9 +2,17 @@ import { BaseModel, types as modelTypes } from '../models'; import * as models from '../models'; import { getBodyBuffer } from '../models/response'; import { Settings } from '../models/settings'; -import { send } from '../network/network'; +import { isWorkspace } from '../models/workspace'; +import { + responseTransform, + sendCurlAndWriteTimeline, + tryToInterpolateRequest, + tryToTransformRequestWithPlugins, +} from '../network/network'; import * as plugins from '../plugins'; +import { invariant } from '../utils/invariant'; import { database } from './database'; +import { RENDER_PURPOSE_SEND } from './render'; // The network layer uses settings from the settings model // We want to give consumers the ability to override certain settings @@ -35,14 +43,48 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB: upsert: docs, remove: [], }); + const fetchInsoRequestData = async (requestId: string) => { + const request = await models.request.getById(requestId); + invariant(request, 'failed to find request'); + const ancestors = await database.withAncestors(request, [ + models.request.type, + models.requestGroup.type, + models.workspace.type, + ]); + const workspaceDoc = ancestors.find(isWorkspace); + const workspaceId = workspaceDoc ? workspaceDoc._id : 'n/a'; + const workspace = await models.workspace.getById(workspaceId); + invariant(workspace, 'failed to find workspace'); + const settings = await models.settings.getOrCreate(); + invariant(settings, 'failed to create settings'); + const clientCertificates = await models.clientCertificate.findByParentId(workspaceId); + const caCert = await models.caCertificate.findByParentId(workspaceId); + + return { request, settings, clientCertificates, caCert }; + }; // Return callback helper to send requests return async function sendRequest(requestId: string) { try { plugins.ignorePlugin('insomnia-plugin-kong-declarative-config'); plugins.ignorePlugin('insomnia-plugin-kong-kubernetes-config'); plugins.ignorePlugin('insomnia-plugin-kong-portal'); - const res = await send(requestId, environmentId); + const { + request, + settings, + clientCertificates, + caCert, + } = await fetchInsoRequestData(requestId); + // NOTE: inso ignores active environment, using the one passed in + const renderResult = await tryToInterpolateRequest(request, environmentId, RENDER_PURPOSE_SEND); + const renderedRequest = await tryToTransformRequestWithPlugins(renderResult); + const response = await sendCurlAndWriteTimeline( + renderedRequest, + clientCertificates, + caCert, + settings, + ); + const res = await responseTransform(response, renderedRequest, renderResult.context); const { statusCode: status, statusMessage, headers: headerArray, elapsedTime: responseTime } = res; const headers = headerArray?.reduce((acc, { name, value }) => ({ ...acc, [name.toLowerCase() || '']: value || '' }), []); const bodyBuffer = await getBodyBuffer(res) as Buffer; diff --git a/packages/insomnia/src/main/network/libcurl-promise.ts b/packages/insomnia/src/main/network/libcurl-promise.ts index e451dc42f..155df57a5 100644 --- a/packages/insomnia/src/main/network/libcurl-promise.ts +++ b/packages/insomnia/src/main/network/libcurl-promise.ts @@ -1,8 +1,7 @@ // NOTE: this file should not be imported by electron renderer because node-libcurl is not-context-aware // Related issue https://github.com/JCMais/node-libcurl/issues/155 -if (process.type === 'renderer') { - throw new Error('node-libcurl unavailable in renderer'); -} +import { invariant } from '../../utils/invariant'; +invariant(process.type !== 'renderer', 'Native abstractions for Nodejs module unavailable in renderer'); import { Curl, CurlAuth, CurlCode, CurlFeature, CurlHttpVersion, CurlInfoDebug, CurlNetrc } from '@getinsomnia/node-libcurl'; import electron from 'electron'; @@ -22,7 +21,7 @@ import { ResponseHeader } from '../../models/response'; import { buildMultipart } from './multipart'; import { parseHeaderStrings } from './parse-header-strings'; -interface CurlRequestOptions { +export interface CurlRequestOptions { requestId: string; // for cancellation req: RequestUsedHere; finalUrl: string; @@ -63,7 +62,7 @@ export interface ResponseTimelineEntry { value: string; } -interface CurlRequestOutput { +export interface CurlRequestOutput { patch: ResponsePatch; debugTimeline: ResponseTimelineEntry[]; headerResults: HeaderResult[]; @@ -234,9 +233,7 @@ export const curlRequest = (options: CurlRequestOptions) => new Promise new Promise responseBodyWriteStream.end()); - curl.on('error', async function(err, code) { + curl.on('error', async (err, code) => { const elapsedTime = curl.getInfo(Curl.info.TOTAL_TIME) as number * 1000; curl.close(); await waitForStreamToFinish(responseBodyWriteStream); @@ -380,7 +377,7 @@ const closeReadFunction = (fd: number, isMultipart: boolean, path?: string) => { } }; -interface HeaderResult { +export interface HeaderResult { headers: ResponseHeader[]; version: string; code: number; diff --git a/packages/insomnia/src/network/__tests__/authentication.test.ts b/packages/insomnia/src/network/__tests__/authentication.test.ts index f9e16fe00..45e1f1b09 100644 --- a/packages/insomnia/src/network/__tests__/authentication.test.ts +++ b/packages/insomnia/src/network/__tests__/authentication.test.ts @@ -197,12 +197,8 @@ describe('API Key', () => { value: 'test', addTo: 'queryParams', }; - const request = { - url: 'https://insomnia.rest/', - method: 'GET', - authentication, - }; - const header = await getAuthQueryParams(request, 'https://insomnia.rest/'); + + const header = getAuthQueryParams(authentication, 'https://insomnia.rest/'); expect(header).toEqual({ 'name': 'x-api-key', 'value': 'test', diff --git a/packages/insomnia/src/network/__tests__/network.test.ts b/packages/insomnia/src/network/__tests__/network.test.ts index fe56f9843..e5c99a556 100644 --- a/packages/insomnia/src/network/__tests__/network.test.ts +++ b/packages/insomnia/src/network/__tests__/network.test.ts @@ -28,7 +28,7 @@ window.app = electron.app; const getRenderedRequest = async (args: Parameters[0]) => (await getRenderedRequestAndContext(args)).request; -describe('actuallySend()', () => { +describe('sendCurlAndWriteTimeline()', () => { beforeEach(async () => { await globalBeforeEach(); await models.project.all(); @@ -101,9 +101,10 @@ describe('actuallySend()', () => { }, }); const renderedRequest = await getRenderedRequest({ request }); - const response = await networkUtils._actuallySend( + const response = await networkUtils.sendCurlAndWriteTimeline( renderedRequest, - '', + [], + null, settings, ); const bodyBuffer = models.response.getBodyBuffer(response); @@ -176,9 +177,10 @@ describe('actuallySend()', () => { url: 'http://localhost', }); const renderedRequest = await getRenderedRequest({ request }); - const response = await networkUtils._actuallySend( + const response = await networkUtils.sendCurlAndWriteTimeline( renderedRequest, - '', + [], + null, settings, ); const bodyBuffer = models.response.getBodyBuffer(response); @@ -276,9 +278,10 @@ describe('actuallySend()', () => { settingSendCookies: false, }); const renderedRequest = await getRenderedRequest({ request }); - const response = await networkUtils._actuallySend( + const response = await networkUtils.sendCurlAndWriteTimeline( renderedRequest, - '', + [], + null, settings, ); const bodyBuffer = models.response.getBodyBuffer(response); @@ -336,9 +339,10 @@ describe('actuallySend()', () => { }, }); const renderedRequest = await getRenderedRequest({ request }); - const response = await networkUtils._actuallySend( + const response = await networkUtils.sendCurlAndWriteTimeline( renderedRequest, - '', + [], + null, settings, ); const bodyBuffer = models.response.getBodyBuffer(response); @@ -416,9 +420,10 @@ describe('actuallySend()', () => { }, }); const renderedRequest = await getRenderedRequest({ request }); - const response = await networkUtils._actuallySend( + const response = await networkUtils.sendCurlAndWriteTimeline( renderedRequest, - '', + [], + null, settings, ); const bodyBuffer = models.response.getBodyBuffer(response); @@ -477,9 +482,10 @@ describe('actuallySend()', () => { method: 'GET', }); const renderedRequest = await getRenderedRequest({ request }); - const response = await networkUtils._actuallySend( + const response = await networkUtils.sendCurlAndWriteTimeline( renderedRequest, - '', + [], + null, settings, ); const bodyBuffer = models.response.getBodyBuffer(response); @@ -517,9 +523,10 @@ describe('actuallySend()', () => { method: 'HEAD', }); const renderedRequest = await getRenderedRequest({ request }); - const response = await networkUtils._actuallySend( + const response = await networkUtils.sendCurlAndWriteTimeline( renderedRequest, - '', + [], + null, settings, ); const bodyBuffer = models.response.getBodyBuffer(response); @@ -556,9 +563,10 @@ describe('actuallySend()', () => { method: 'GET', }); const renderedRequest = await getRenderedRequest({ request }); - const response = await networkUtils._actuallySend( + const response = await networkUtils.sendCurlAndWriteTimeline( renderedRequest, - '', + [], + null, settings, ); const bodyBuffer = models.response.getBodyBuffer(response); @@ -596,9 +604,10 @@ describe('actuallySend()', () => { }, }); const renderedRequest = await getRenderedRequest({ request }); - const response = await networkUtils._actuallySend( + const response = await networkUtils.sendCurlAndWriteTimeline( renderedRequest, - '', + [], + null, settings, ); const bodyBuffer = models.response.getBodyBuffer(response); @@ -693,9 +702,10 @@ describe('actuallySend()', () => { }, }); const renderedRequest = await getRenderedRequest({ request }); - const response = await networkUtils._actuallySend( + const response = await networkUtils.sendCurlAndWriteTimeline( renderedRequest, - '', + [], + null, { ...settings, validateSSL: false }, ); const bodyBuffer = models.response.getBodyBuffer(response); @@ -745,7 +755,7 @@ describe('actuallySend()', () => { parentId: workspace._id, }); const renderedRequest = await getRenderedRequest({ request }); - const responseV1 = await networkUtils._actuallySend(renderedRequest, '', { + const responseV1 = await networkUtils.sendCurlAndWriteTimeline(renderedRequest, [], null, { ...settings, preferredHttpVersion: HttpVersions.V1_0, }); diff --git a/packages/insomnia/src/network/authentication.ts b/packages/insomnia/src/network/authentication.ts index cd940688d..c187b39b2 100644 --- a/packages/insomnia/src/network/authentication.ts +++ b/packages/insomnia/src/network/authentication.ts @@ -11,7 +11,7 @@ import { AUTH_OAUTH_2, } from '../common/constants'; import type { RenderedRequest } from '../common/render'; -import { RequestParameter } from '../models/request'; +import { RequestAuthentication, RequestParameter } from '../models/request'; import { COOKIE, HEADER, QUERY_PARAMS } from './api-key/constants'; import { getBasicAuthHeader } from './basic-auth/get-header'; import { getBearerAuthHeader } from './bearer-auth/get-header'; @@ -160,9 +160,7 @@ export async function getAuthHeader(renderedRequest: RenderedRequest, url: strin return; } -export async function getAuthQueryParams(renderedRequest: RenderedRequest) { - const { authentication } = renderedRequest; - +export function getAuthQueryParams(authentication: RequestAuthentication) { if (authentication.disabled) { return; } diff --git a/packages/insomnia/src/network/cancellation.ts b/packages/insomnia/src/network/cancellation.ts new file mode 100644 index 000000000..5f1681234 --- /dev/null +++ b/packages/insomnia/src/network/cancellation.ts @@ -0,0 +1,44 @@ +import type { CurlRequestOptions, CurlRequestOutput } from '../main/network/libcurl-promise'; +const cancelRequestFunctionMap = new Map void>(); +export async function cancelRequestById(requestId: string) { + const cancel = cancelRequestFunctionMap.get(requestId); + if (cancel) { + return cancel(); + } + console.log(`[network] Failed to cancel req=${requestId} because cancel function not found`); +} +export const cancellableCurlRequest = async (requestOptions: CurlRequestOptions) => { + const requestId = requestOptions.requestId; + const controller = new AbortController(); + const cancelRequest = () => { + window.main.cancelCurlRequest(requestId); + controller.abort(); + }; + cancelRequestFunctionMap.set(requestId, cancelRequest); + try { + const result = await cancellablePromise({ signal: controller.signal, fn: window.main.curlRequest(requestOptions) }); + return result as CurlRequestOutput; + } catch (err) { + cancelRequestFunctionMap.delete(requestId); + if (err.name === 'AbortError') { + return { statusMessage: 'Cancelled', error: 'Request was cancelled' }; + } + console.log('[network] Error', err); + return { statusMessage: 'Error', error: err.message || 'Something went wrong' }; + } +}; +const cancellablePromise = ({ signal, fn }: { signal: AbortSignal; fn: Promise }) => { + if (signal?.aborted) { + return Promise.reject(new DOMException('Aborted', 'AbortError')); + } + return new Promise((resolve, reject) => { + const abortHandler = () => { + reject(new DOMException('Aborted', 'AbortError')); + }; + fn.then(res => { + resolve(res); + signal?.removeEventListener('abort', abortHandler); + }, reject); + signal?.addEventListener('abort', abortHandler); + }); +}; diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index 241f66f96..6b270e1d4 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -4,33 +4,31 @@ import mkdirp from 'mkdirp'; import { join as pathJoin } from 'path'; import { v4 as uuidv4 } from 'uuid'; -import { - STATUS_CODE_PLUGIN_ERROR, -} from '../common/constants'; import { cookiesFromJar, jarFromCookies } from '../common/cookies'; import { database as db } from '../common/database'; import { getDataDirectory } from '../common/electron-helpers'; import { - delay, getContentTypeHeader, getLocationHeader, getSetCookieHeaders, } from '../common/misc'; -import type { ExtraRenderInfo, RenderedRequest } from '../common/render'; +import type { ExtraRenderInfo, RenderedRequest, RenderPurpose, RequestAndContext } from '../common/render'; import { getRenderedRequestAndContext, RENDER_PURPOSE_NO_RENDER, RENDER_PURPOSE_SEND, } from '../common/render'; -import type { ResponsePatch, ResponseTimelineEntry } from '../main/network/libcurl-promise'; +import type { HeaderResult, ResponsePatch, ResponseTimelineEntry } from '../main/network/libcurl-promise'; import * as models from '../models'; -import { Cookie, CookieJar } from '../models/cookie-jar'; -import type { Environment } from '../models/environment'; -import type { Request } from '../models/request'; +import { CaCertificate } from '../models/ca-certificate'; +import { ClientCertificate } from '../models/client-certificate'; +import { Cookie } from '../models/cookie-jar'; +import type { Request, RequestAuthentication, RequestParameter } from '../models/request'; import type { Settings } from '../models/settings'; import { isWorkspace } from '../models/workspace'; import * as pluginContexts from '../plugins/context/index'; import * as plugins from '../plugins/index'; +import { invariant } from '../utils/invariant'; import { setDefaultProtocol } from '../utils/url/protocol'; import { buildQueryStringFromParams, @@ -38,160 +36,243 @@ import { smartEncodeUrl, } from '../utils/url/querystring'; import { getAuthHeader, getAuthQueryParams } from './authentication'; +import { cancellableCurlRequest } from './cancellation'; import { urlMatchesCertHost } from './url-matches-cert-host'; -// Time since user's last keypress to wait before making the request -const MAX_DELAY_TIME = 1000; +// used for oauth grant types +// creates a new request with the patch args +// and uses env and settings from workspace +// not cancellable but currently is +// used indirectly by send and getAuthHeader to fetch tokens +// @TODO unpack oauth into regular timeline and remove oauth timeine dialog +export async function sendWithSettings( + requestId: string, + requestPatch: Record, +) { + console.log(`[network] Sending with settings req=${requestId}`); + const { request, + environment, + settings, + clientCertificates, + caCert } = await fetchRequestData(requestId); -const cancelRequestFunctionMap: Record void> = {}; + const newRequest: Request = await models.initModel(models.request.type, requestPatch, { + _id: request._id + '.other', + parentId: request._id, + }); -let lastUserInteraction = Date.now(); + const renderResult = await tryToInterpolateRequest(newRequest, environment._id); + const renderedRequest = await tryToTransformRequestWithPlugins(renderResult); -export async function cancelRequestById(requestId: string) { - const hasCancelFunction = cancelRequestFunctionMap.hasOwnProperty(requestId) && typeof cancelRequestFunctionMap[requestId] === 'function'; - if (hasCancelFunction) { - return cancelRequestFunctionMap[requestId](); - } - console.log(`[network] Failed to cancel req=${requestId} because cancel function not found`); + const response = await sendCurlAndWriteTimeline( + renderResult.request, + clientCertificates, + caCert, + { ...settings, validateSSL: settings.validateAuthSSL }, + ); + return responseTransform(response, renderedRequest, renderResult.context); } -export async function _actuallySend( +// used by test feature, inso, and plugin api +// not all need to be cancellable or to use curl +export async function send( + requestId: string, + environmentId?: string, + extraInfo?: ExtraRenderInfo, +) { + console.log(`[network] Sending req=${requestId} env=${environmentId || 'null'}`); + + const { request, + environment, + settings, + clientCertificates, + caCert } = await fetchRequestData(requestId); + + const renderResult = await tryToInterpolateRequest(request, environment._id, RENDER_PURPOSE_SEND, extraInfo); + const renderedRequest = await tryToTransformRequestWithPlugins(renderResult); + const response = await sendCurlAndWriteTimeline( + renderedRequest, + clientCertificates, + caCert, + settings, + ); + return responseTransform(response, renderedRequest, renderResult.context); +} + +const fetchRequestData = async (requestId: string) => { + const request = await models.request.getById(requestId); + invariant(request, 'failed to find request'); + const ancestors = await db.withAncestors(request, [ + models.request.type, + models.requestGroup.type, + models.workspace.type, + ]); + const workspaceDoc = ancestors.find(isWorkspace); + const workspaceId = workspaceDoc ? workspaceDoc._id : 'n/a'; + const workspace = await models.workspace.getById(workspaceId); + invariant(workspace, 'failed to find workspace'); + const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); + + // fallback to base environment + const environment = workspaceMeta.activeEnvironmentId ? + await models.environment.getById(workspaceMeta.activeEnvironmentId) + : await models.environment.getOrCreateForParentId(workspace._id); + invariant(environment, 'failed to find environment'); + + const settings = await models.settings.getOrCreate(); + invariant(settings, 'failed to create settings'); + const clientCertificates = await models.clientCertificate.findByParentId(workspaceId); + const caCert = await models.caCertificate.findByParentId(workspaceId); + + return { request, environment, settings, clientCertificates, caCert }; +}; + +export const tryToInterpolateRequest = async (request: Request, environmentId: string, purpose?: RenderPurpose, extraInfo?: ExtraRenderInfo) => { + try { + return await getRenderedRequestAndContext({ + request: request, + environmentId, + purpose, + extraInfo, + }); + } catch (err) { + throw new Error(`Failed to render request: ${request._id}`); + } +}; +export const tryToTransformRequestWithPlugins = async (renderResult: RequestAndContext) => { + const { request, context } = renderResult; + try { + return await _applyRequestPluginHooks(request, context); + } catch (err) { + throw new Error(`Failed to transform request with plugins: ${request._id}`); + } +}; +export async function sendCurlAndWriteTimeline( renderedRequest: RenderedRequest, - workspaceId: string, + clientCertificates: ClientCertificate[], + caCert: CaCertificate | null, settings: Settings, ) { - return new Promise(async resolve => { - const timeline: ResponseTimelineEntry[] = []; + const requestId = renderedRequest._id; + const timeline: ResponseTimelineEntry[] = []; - try { - // Setup the cancellation logic - cancelRequestFunctionMap[renderedRequest._id] = async () => { - const timelinePath = await storeTimeline(timeline); - // Tear Down the cancellation logic - if (cancelRequestFunctionMap.hasOwnProperty(renderedRequest._id)) { - delete cancelRequestFunctionMap[renderedRequest._id]; - } - // NOTE: conditionally use ipc bridge, renderer cannot import native modules directly - const nodejsCancelCurlRequest = process.type === 'renderer' - ? window.main.cancelCurlRequest - : (await import('../main/network/libcurl-promise')).cancelCurlRequest; + const { finalUrl, socketPath } = transformUrl(renderedRequest.url, renderedRequest.parameters, renderedRequest.authentication, renderedRequest.settingEncodeUrl); - nodejsCancelCurlRequest(renderedRequest._id); - return resolve({ - elapsedTime: 0, - bytesRead: 0, - url: renderedRequest.url, - statusMessage: 'Cancelled', - error: 'Request was cancelled', - timelinePath, - }); - }; - const authQueryParam = await getAuthQueryParams(renderedRequest); - // Set the URL, including the query parameters - const qs = buildQueryStringFromParams( - authQueryParam - ? renderedRequest.parameters.concat([authQueryParam]) - : renderedRequest.parameters - ); - const url = joinUrlAndQueryString(renderedRequest.url, qs); - const isUnixSocket = url.match(/https?:\/\/unix:\//); - let finalUrl, socketPath; - if (!isUnixSocket) { - finalUrl = smartEncodeUrl(url, renderedRequest.settingEncodeUrl); - } else { - // URL prep will convert "unix:/path" hostname to "unix/path" - const match = smartEncodeUrl(url, renderedRequest.settingEncodeUrl).match(/(https?:)\/\/unix:?(\/[^:]+):\/(.+)/); - const protocol = (match && match[1]) || ''; - socketPath = (match && match[2]) || ''; - const socketUrl = (match && match[3]) || ''; - finalUrl = `${protocol}//${socketUrl}`; - } - timeline.push({ value: `Preparing request to ${finalUrl}`, name: 'Text', timestamp: Date.now() }); - timeline.push({ value: `Current time is ${new Date().toISOString()}`, name: 'Text', timestamp: Date.now() }); - timeline.push({ value: `${renderedRequest.settingEncodeUrl ? 'Enable' : 'Disable'} automatic URL encoding`, name: 'Text', timestamp: Date.now() }); + timeline.push({ value: `Preparing request to ${finalUrl}`, name: 'Text', timestamp: Date.now() }); + timeline.push({ value: `Current time is ${new Date().toISOString()}`, name: 'Text', timestamp: Date.now() }); + timeline.push({ value: `${renderedRequest.settingEncodeUrl ? 'Enable' : 'Disable'} automatic URL encoding`, name: 'Text', timestamp: Date.now() }); - if (!renderedRequest.settingSendCookies) { - timeline.push({ value: 'Disable cookie sending due to user setting', name: 'Text', timestamp: Date.now() }); - } - const clientCertificates = await models.clientCertificate.findByParentId(workspaceId); - const certificates = clientCertificates.filter(c => !c.disabled && urlMatchesCertHost(setDefaultProtocol(c.host, 'https:'), renderedRequest.url)); - const caCert = await models.caCertificate.findByParentId(workspaceId); - const caCertficatePath = caCert?.disabled === false ? caCert.path : null; - const authHeader = await getAuthHeader(renderedRequest, finalUrl); + if (!renderedRequest.settingSendCookies) { + timeline.push({ value: 'Disable cookie sending due to user setting', name: 'Text', timestamp: Date.now() }); + } - // NOTE: conditionally use ipc bridge, renderer cannot import native modules directly - const nodejsCurlRequest = process.type === 'renderer' - ? window.main.curlRequest - : (await import('../main/network/libcurl-promise')).curlRequest; + const authHeader = await getAuthHeader(renderedRequest, finalUrl); + const requestOptions = { + requestId, + req: renderedRequest, + finalUrl, + socketPath, + settings, + certificates: clientCertificates.filter(c => !c.disabled && urlMatchesCertHost(setDefaultProtocol(c.host, 'https:'), renderedRequest.url)), + caCertficatePath: caCert?.disabled === false ? caCert.path : null, + authHeader, + }; - const requestOptions = { - requestId: renderedRequest._id, - req: renderedRequest, - finalUrl, - socketPath, - settings, - certificates, - caCertficatePath, - authHeader, - }; - const { patch, debugTimeline, headerResults, responseBodyPath } = await nodejsCurlRequest(requestOptions); - const { cookieJar, settingStoreCookies } = renderedRequest; + // NOTE: conditionally use ipc bridge, renderer cannot import native modules directly + const nodejsCurlRequest = process.type === 'renderer' + ? cancellableCurlRequest + : (await import('../main/network/libcurl-promise')).curlRequest; + const output = await nodejsCurlRequest(requestOptions); - // add set-cookie headers to file(cookiejar) and database - if (settingStoreCookies) { - // supports many set-cookies over many redirects - const redirects: string[][] = headerResults.map(({ headers }: any) => getSetCookiesFromResponseHeaders(headers)); - const setCookieStrings: string[] = redirects.flat(); - const totalSetCookies = setCookieStrings.length; - if (totalSetCookies) { - const currentUrl = getCurrentUrl({ headerResults, finalUrl }); - const { cookies, rejectedCookies } = await addSetCookiesToToughCookieJar({ setCookieStrings, currentUrl, cookieJar }); - rejectedCookies.forEach(errorMessage => timeline.push({ value: `Rejected cookie: ${errorMessage}`, name: 'Text', timestamp: Date.now() })); - const hasCookiesToPersist = totalSetCookies > rejectedCookies.length; - if (hasCookiesToPersist) { - const patch: Partial = { cookies }; - await models.cookieJar.update(cookieJar, patch); - timeline.push({ value: `Saved ${totalSetCookies} cookies`, name: 'Text', timestamp: Date.now() }); - } - } - } + if ('error' in output) { + const timelinePath = await storeTimeline(timeline); - const lastRedirect = headerResults[headerResults.length - 1]; - const responsePatch: ResponsePatch = { - contentType: getContentTypeHeader(lastRedirect.headers)?.value || '', - headers: lastRedirect.headers, - httpVersion: lastRedirect.version, - statusCode: lastRedirect.code, - statusMessage: lastRedirect.reason, - ...patch, - }; - const timelinePath = await storeTimeline([...timeline, ...debugTimeline]); - // Tear Down the cancellation logic - if (cancelRequestFunctionMap.hasOwnProperty(renderedRequest._id)) { - delete cancelRequestFunctionMap[renderedRequest._id]; - } - return resolve({ - timelinePath, - bodyPath: responseBodyPath, - ...responsePatch, - }); - } catch (err) { - console.log('[network] Error', err); - const timelinePath = await storeTimeline(timeline); - // Tear Down the cancellation logic - if (cancelRequestFunctionMap.hasOwnProperty(renderedRequest._id)) { - delete cancelRequestFunctionMap[renderedRequest._id]; - } - return resolve({ - url: renderedRequest.url, - error: err.message || 'Something went wrong', - elapsedTime: 0, // 0 because this path is hit during plugin calls - statusMessage: 'Error', - timelinePath, - }); - } - }); + return { + parentId: requestId, + url: requestOptions.finalUrl, + error: output.error, + elapsedTime: 0, // 0 because this path is hit during plugin calls + bytesRead: 0, + statusMessage: output.statusMessage, + timelinePath, + }; + } + const { patch, debugTimeline, headerResults, responseBodyPath } = output; + const timelinePath = await storeTimeline([...timeline, ...debugTimeline]); + // transform output + const { cookies, rejectedCookies, totalSetCookies } = await extractCookies(headerResults, renderedRequest.cookieJar, finalUrl, renderedRequest.settingStoreCookies); + rejectedCookies.forEach(errorMessage => timeline.push({ value: `Rejected cookie: ${errorMessage}`, name: 'Text', timestamp: Date.now() })); + if (cookies) { + await models.cookieJar.update(renderedRequest.cookieJar, { cookies }); + timeline.push({ value: `Saved ${totalSetCookies} cookies`, name: 'Text', timestamp: Date.now() }); + } + const lastRedirect = headerResults[headerResults.length - 1]; + + return { + parentId: renderedRequest._id, + timelinePath, + bodyPath: responseBodyPath, + contentType: getContentTypeHeader(lastRedirect.headers)?.value || '', + headers: lastRedirect.headers, + httpVersion: lastRedirect.version, + statusCode: lastRedirect.code, + statusMessage: lastRedirect.reason, + ...patch, + }; } +export const responseTransform = (patch: ResponsePatch, renderedRequest: RenderedRequest, context: Record) => { + const response = { + ...patch, + environmentId: patch.environmentId, + bodyCompression: null, + settingSendCookies: renderedRequest.settingSendCookies, + settingStoreCookies: renderedRequest.settingStoreCookies, + }; + + if (response.error) { + console.log(`[network] Response failed req=${patch.parentId} err=${response.error || 'n/a'}`); + return response; + } + console.log(`[network] Response succeeded req=${patch.parentId} status=${response.statusCode || '?'}`,); + return _applyResponsePluginHooks( + response, + renderedRequest, + context, + ); +}; +export const transformUrl = (url: string, params: RequestParameter[], authentication: RequestAuthentication, shouldEncode: boolean) => { + const authQueryParam = getAuthQueryParams(authentication); + const customUrl = joinUrlAndQueryString(url, buildQueryStringFromParams(authQueryParam ? params.concat([authQueryParam]) : params)); + const isUnixSocket = customUrl.match(/https?:\/\/unix:\//); + if (!isUnixSocket) { + return { finalUrl: smartEncodeUrl(customUrl, shouldEncode) }; + } + // URL prep will convert "unix:/path" hostname to "unix/path" + const match = smartEncodeUrl(customUrl, shouldEncode).match(/(https?:)\/\/unix:?(\/[^:]+):\/(.+)/); + const protocol = (match && match[1]) || ''; + const socketPath = (match && match[2]) || ''; + const socketUrl = (match && match[3]) || ''; + return { finalUrl: `${protocol}//${socketUrl}`, socketPath }; +}; + +const extractCookies = async (headerResults: HeaderResult[], cookieJar: any, finalUrl: string, settingStoreCookies: boolean) => { + // add set-cookie headers to file(cookiejar) and database + if (settingStoreCookies) { + // supports many set-cookies over many redirects + const redirects: string[][] = headerResults.map(({ headers }: any) => getSetCookiesFromResponseHeaders(headers)); + const setCookieStrings: string[] = redirects.flat(); + const totalSetCookies = setCookieStrings.length; + if (totalSetCookies) { + const currentUrl = getCurrentUrl({ headerResults, finalUrl }); + const { cookies, rejectedCookies } = await addSetCookiesToToughCookieJar({ setCookieStrings, currentUrl, cookieJar }); + const hasCookiesToPersist = totalSetCookies > rejectedCookies.length; + if (hasCookiesToPersist) { + return { cookies, rejectedCookies, totalSetCookies }; + } + } + } + return { cookies: [], rejectedCookies: [], totalSetCookies: 0 }; +}; export const getSetCookiesFromResponseHeaders = (headers: any[]) => getSetCookieHeaders(headers).map(h => h.value); @@ -227,169 +308,6 @@ export const addSetCookiesToToughCookieJar = async ({ setCookieStrings, currentU return { cookies, rejectedCookies }; }; -export async function sendWithSettings( - requestId: string, - requestPatch: Record, -) { - console.log(`[network] Sending with settings req=${requestId}`); - const request = await models.request.getById(requestId); - - if (!request) { - throw new Error(`Failed to find request: ${requestId}`); - } - const settings = await models.settings.getOrCreate(); - const ancestors = await db.withAncestors(request, [ - models.request.type, - models.requestGroup.type, - models.workspace.type, - ]); - const workspaceDoc = ancestors.find(isWorkspace); - const workspaceId = workspaceDoc ? workspaceDoc._id : 'n/a'; - const workspace = await models.workspace.getById(workspaceId); - - if (!workspace) { - throw new Error(`Failed to find workspace for: ${requestId}`); - } - - const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); - const environmentId: string = workspaceMeta.activeEnvironmentId || 'n/a'; - const newRequest: Request = await models.initModel(models.request.type, requestPatch, { - _id: request._id + '.other', - parentId: request._id, - }); - let renderResult: { - request: RenderedRequest; - context: Record; - }; - try { - renderResult = await getRenderedRequestAndContext({ request: newRequest, environmentId }); - } catch (err) { - throw new Error(`Failed to render request: ${requestId}`); - } - - const environment: Environment | null = await models.environment.getById(environmentId || 'n/a'); - const responseEnvironmentId = environment ? environment._id : null; - - const response = await _actuallySend( - renderResult.request, - workspace._id, - { ...settings, validateSSL: settings.validateAuthSSL }, - ); - response.parentId = renderResult.request._id; - response.environmentId = responseEnvironmentId; - response.bodyCompression = null; - response.settingSendCookies = renderResult.request.settingSendCookies; - response.settingStoreCookies = renderResult.request.settingStoreCookies; - if (response.error) { - return response; - } - return _applyResponsePluginHooks( - response, - renderResult.request, - renderResult.context, - ); -} - -export async function send( - requestId: string, - environmentId?: string, - extraInfo?: ExtraRenderInfo, -) { - console.log(`[network] Sending req=${requestId} env=${environmentId || 'null'}`); - // HACK: wait for all debounces to finish - - /* - * TODO: Do this in a more robust way - * The following block adds a "long" delay to let potential debounces and - * database updates finish before making the request. This is done by tracking - * the time of the user's last keypress and making sure the request is sent a - * significant time after the last press. - */ - const timeSinceLastInteraction = Date.now() - lastUserInteraction; - const delayMillis = Math.max(0, MAX_DELAY_TIME - timeSinceLastInteraction); - if (delayMillis > 0) { - await delay(delayMillis); - } - - // Fetch some things - - const request = await models.request.getById(requestId); - const settings = await models.settings.getOrCreate(); - const ancestors = await db.withAncestors(request, [ - models.request.type, - models.requestGroup.type, - models.workspace.type, - ]); - - if (!request) { - throw new Error(`Failed to find request to send for ${requestId}`); - } - const renderResult = await getRenderedRequestAndContext( - { - request, - environmentId, - purpose: RENDER_PURPOSE_SEND, - extraInfo, - }, - ); - - const renderedRequestBeforePlugins = renderResult.request; - const renderedContextBeforePlugins = renderResult.context; - const workspaceDoc = ancestors.find(isWorkspace); - const workspace = await models.workspace.getById(workspaceDoc ? workspaceDoc._id : 'n/a'); - - if (!workspace) { - throw new Error(`Failed to find workspace for request: ${requestId}`); - } - - const environment: Environment | null = await models.environment.getById(environmentId || 'n/a'); - const responseEnvironmentId = environment ? environment._id : null; - - let renderedRequest: RenderedRequest; - - try { - renderedRequest = await _applyRequestPluginHooks( - renderedRequestBeforePlugins, - renderedContextBeforePlugins, - ); - } catch (err) { - return { - environmentId: responseEnvironmentId, - error: err.message || 'Something went wrong', - parentId: renderedRequestBeforePlugins._id, - settingSendCookies: renderedRequestBeforePlugins.settingSendCookies, - settingStoreCookies: renderedRequestBeforePlugins.settingStoreCookies, - statusCode: STATUS_CODE_PLUGIN_ERROR, - statusMessage: err.plugin ? `Plugin ${err.plugin.name}` : 'Plugin', - url: renderedRequestBeforePlugins.url, - } as ResponsePatch; - } - const response = await _actuallySend( - renderedRequest, - workspace._id, - settings, - ); - response.parentId = renderResult.request._id; - response.environmentId = responseEnvironmentId; - response.bodyCompression = null; - response.settingSendCookies = renderedRequest.settingSendCookies; - response.settingStoreCookies = renderedRequest.settingStoreCookies; - - console.log( - response.error - ? `[network] Response failed req=${requestId} err=${response.error || 'n/a'}` - : `[network] Response succeeded req=${requestId} status=${response.statusCode || '?'}`, - ); - if (response.error) { - return response; - } - return _applyResponsePluginHooks( - response, - renderedRequest, - renderedContextBeforePlugins, - ); -} - async function _applyRequestPluginHooks( renderedRequest: RenderedRequest, renderedContext: Record, @@ -475,16 +393,3 @@ export function storeTimeline(timeline: ResponseTimelineEntry[]): Promise { - if (event.ctrlKey || event.metaKey || event.altKey) { - return; - } - - lastUserInteraction = Date.now(); - }); - document.addEventListener('paste', () => { - lastUserInteraction = Date.now(); - }); -} diff --git a/packages/insomnia/src/network/unit-test-feature.ts b/packages/insomnia/src/network/unit-test-feature.ts index ac1c4f6ef..6391f8479 100644 --- a/packages/insomnia/src/network/unit-test-feature.ts +++ b/packages/insomnia/src/network/unit-test-feature.ts @@ -10,6 +10,7 @@ export function getSendRequestCallback(environmentId?: string) { plugins.ignorePlugin('insomnia-plugin-kong-declarative-config'); plugins.ignorePlugin('insomnia-plugin-kong-kubernetes-config'); plugins.ignorePlugin('insomnia-plugin-kong-portal'); + // NOTE: unit tests will use the UI selected environment const res = await send(requestId, environmentId); const { statusCode: status, statusMessage, headers: headerArray, elapsedTime: responseTime } = res; const headers = headerArray?.reduce((acc, { name, value }) => ({ ...acc, [name.toLowerCase() || '']: value || '' }), []); diff --git a/packages/insomnia/src/ui/components/panes/response-pane.tsx b/packages/insomnia/src/ui/components/panes/response-pane.tsx index 583a29ede..5219ec9b1 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/response-pane.tsx @@ -9,7 +9,7 @@ import { getSetCookieHeaders } from '../../../common/misc'; import * as models from '../../../models'; import type { Request } from '../../../models/request'; import type { Response } from '../../../models/response'; -import { cancelRequestById } from '../../../network/network'; +import { cancelRequestById } from '../../../network/cancellation'; import { jsonPrettify } from '../../../utils/prettify/json'; import { updateRequestMetaByParentId } from '../../hooks/create-request'; import { selectActiveResponse, selectResponseFilter, selectResponseFilterHistory, selectResponsePreviewMode, selectSettings } from '../../redux/selectors';