diff --git a/packages/insomnia-app/app/common/__tests__/render.test.ts b/packages/insomnia-app/app/common/__tests__/render.test.ts index 2a47a5839..fbc859c19 100644 --- a/packages/insomnia-app/app/common/__tests__/render.test.ts +++ b/packages/insomnia-app/app/common/__tests__/render.test.ts @@ -49,15 +49,11 @@ describe('render tests', () => { expect(rendered).toBe('Hello FooBar!'); }); - it('fails on invalid template', async () => { - try { - await renderUtils.render('Hello {{ msg }!', { - msg: 'World', - }); - fail('Render should have failed'); - } catch (err) { - expect(err.message).toContain('expected variable end'); - } + it('returns invalid template', async () => { + const rendered = await renderUtils.render('Hello {{ msg }!', { + msg: 'World', + }); + expect(rendered).toBe('Hello {{ msg }!'); }); it('handles variables using tag before tag is defined as expected (incorrect order)', async () => { @@ -570,7 +566,7 @@ describe('render tests', () => { ); fail('Render should have failed'); } catch (err) { - expect(err.message).toContain('expected variable end'); + expect(err.message).toContain('attempted to output null or undefined value'); } }); diff --git a/packages/insomnia-app/app/common/misc.ts b/packages/insomnia-app/app/common/misc.ts index 0a4d4160a..2081a0b27 100644 --- a/packages/insomnia-app/app/common/misc.ts +++ b/packages/insomnia-app/app/common/misc.ts @@ -1,6 +1,5 @@ import fuzzysort from 'fuzzysort'; import { join as pathJoin } from 'path'; -import { Readable, Writable } from 'stream'; import * as uuid from 'uuid'; import zlib from 'zlib'; @@ -341,27 +340,6 @@ export function fuzzyMatchAll( }; } -export async function waitForStreamToFinish(stream: Readable | Writable) { - return new Promise(resolve => { - // @ts-expect-error -- access of internal values that are intended to be private. We should _not_ do this. - if (stream._readableState?.finished) { - return resolve(); - } - - // @ts-expect-error -- access of internal values that are intended to be private. We should _not_ do this. - if (stream._writableState?.finished) { - return resolve(); - } - - stream.on('close', () => { - resolve(); - }); - stream.on('error', () => { - resolve(); - }); - }); -} - export function chunkArray(arr: T[], chunkSize: number) { const chunks: T[][] = []; diff --git a/packages/insomnia-app/app/global.d.ts b/packages/insomnia-app/app/global.d.ts index 4ee9ce60c..c4629bab2 100644 --- a/packages/insomnia-app/app/global.d.ts +++ b/packages/insomnia-app/app/global.d.ts @@ -27,6 +27,20 @@ interface Window { authorizeUserInWindow: (options: { url: string; urlSuccessRegex?: RegExp; urlFailureRegex?: RegExp; sessionId: string }) => Promise; setMenuBarVisibility: (visible: boolean) => void; installPlugin: (url: string) => void; + writeFile: (options: {path: string; content: string}) => Promise; + cancelCurlRequest: (requestId: string) => void; + curlRequest: (options: { + curlOptions: CurlOpt[]; + responseBodyPath: string; + maxTimelineDataSizeKB: number; + requestId: string; + requestBodyPath?: string; + isMultipart: boolean; + }) => Promise<{ + patch: ResponsePatch; + debugTimeline: ResponseTimelineEntry[]; + headerResults: HeaderResult[]; + }>; }; dialog: { showOpenDialog: (options: Electron.OpenDialogOptions) => Promise; diff --git a/packages/insomnia-app/app/main.development.ts b/packages/insomnia-app/app/main.development.ts index efc17cce2..a1c3d1347 100644 --- a/packages/insomnia-app/app/main.development.ts +++ b/packages/insomnia-app/app/main.development.ts @@ -1,6 +1,7 @@ import * as electron from 'electron'; import contextMenu from 'electron-context-menu'; import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'; +import { writeFile } from 'fs'; import path from 'path'; import appConfig from '../config/config.json'; @@ -17,6 +18,7 @@ import * as updates from './main/updates'; import * as windowUtils from './main/window-utils'; import * as models from './models/index'; import type { Stats } from './models/stats'; +import { cancelCurlRequest, curlRequest } from './network/libcurl-promise'; import { authorizeUserInWindow } from './network/o-auth-2/misc'; import installPlugin from './plugins/install'; import type { ToastNotification } from './ui/components/toast'; @@ -259,6 +261,25 @@ async function _trackStats() { return authorizeUserInWindow({ url, urlSuccessRegex, urlFailureRegex, sessionId }); }); + ipcMain.handle('writeFile', (_, options) => { + return new Promise((resolve, reject) => { + writeFile(options.path, options.content, err => { + if (err != null) { + return reject(err); + } + resolve(options.path); + }); + }); + }); + + ipcMain.handle('curlRequest', (_, options) => { + return curlRequest(options); + }); + + ipcMain.on('cancelCurlRequest', (_, requestId: string): void => { + cancelCurlRequest(requestId); + }); + ipcMain.once('window-ready', () => { const { currentVersion, launches, lastVersion } = stats; diff --git a/packages/insomnia-app/app/main/window-utils.ts b/packages/insomnia-app/app/main/window-utils.ts index 0d452cb59..82abc2a07 100644 --- a/packages/insomnia-app/app/main/window-utils.ts +++ b/packages/insomnia-app/app/main/window-utils.ts @@ -1,4 +1,3 @@ -import { Curl } from '@getinsomnia/node-libcurl'; import electron, { BrowserWindow, MenuItemConstructorOptions } from 'electron'; import fs from 'fs'; import * as os from 'os'; @@ -22,9 +21,6 @@ import * as log from '../common/log'; import LocalStorage from './local-storage'; const { app, Menu, shell, dialog, clipboard } = electron; -// So we can use native modules in renderer -// NOTE: This was (deprecated in Electron 10)[https://github.com/electron/electron/issues/18397] and (removed in Electron 14)[https://github.com/electron/electron/pull/26874] -app.allowRendererProcessReuse = false; const DEFAULT_WIDTH = 1280; const DEFAULT_HEIGHT = 720; @@ -388,7 +384,6 @@ export function createWindow() { `Node: ${process.versions.node}`, `V8: ${process.versions.v8}`, `Architecture: ${process.arch}`, - `node-libcurl: ${Curl.getVersion()}`, ].join('\n'); const msgBox = await dialog.showMessageBox({ diff --git a/packages/insomnia-app/app/network/__tests__/network.test.ts b/packages/insomnia-app/app/network/__tests__/network.test.ts index 021c56dd8..416e55fab 100644 --- a/packages/insomnia-app/app/network/__tests__/network.test.ts +++ b/packages/insomnia-app/app/network/__tests__/network.test.ts @@ -1,4 +1,5 @@ -import { CurlHttpVersion } from '@getinsomnia/node-libcurl'; +import { CurlHttpVersion } from '@getinsomnia/node-libcurl/dist/enum/CurlHttpVersion'; +import { CurlNetrc } from '@getinsomnia/node-libcurl/dist/enum/CurlNetrc'; import electron from 'electron'; import fs from 'fs'; import { HttpVersions } from 'insomnia-common'; @@ -17,6 +18,7 @@ import { import { filterHeaders } from '../../common/misc'; import { getRenderedRequestAndContext } from '../../common/render'; import * as models from '../../models'; +import { _parseHeaders } from '../libcurl-promise'; import { DEFAULT_BOUNDARY } from '../multipart'; import * as networkUtils from '../network'; window.app = electron.app; @@ -604,7 +606,7 @@ describe('actuallySend()', () => { NOPROGRESS: true, PROXY: '', TIMEOUT_MS: 0, - NETRC: 'Required', + NETRC: CurlNetrc.Required, URL: '', USERAGENT: `insomnia/${getAppVersion()}`, VERBOSE: true, @@ -736,7 +738,7 @@ describe('actuallySend()', () => { ...settings, preferredHttpVersion: HttpVersions.V1_0, }); - expect(JSON.parse(String(models.response.getBodyBuffer(responseV1))).options.HTTP_VERSION).toBe('V1_0'); + expect(JSON.parse(String(models.response.getBodyBuffer(responseV1))).options.HTTP_VERSION).toBe(1); expect(networkUtils.getHttpVersion(HttpVersions.V1_0).curlHttpVersion).toBe(CurlHttpVersion.V1_0); expect(networkUtils.getHttpVersion(HttpVersions.V1_1).curlHttpVersion).toBe(CurlHttpVersion.V1_1); expect(networkUtils.getHttpVersion(HttpVersions.V2PriorKnowledge).curlHttpVersion).toBe(CurlHttpVersion.V2PriorKnowledge); @@ -745,48 +747,6 @@ describe('actuallySend()', () => { expect(networkUtils.getHttpVersion(HttpVersions.default).curlHttpVersion).toBe(undefined); expect(networkUtils.getHttpVersion('blah').curlHttpVersion).toBe(undefined); }); - - it('requests can be cancelled by requestId', async () => { - // GIVEN - const workspace = await models.workspace.create(); - const settings = await models.settings.getOrCreate(); - const request1 = Object.assign(models.request.init(), { - _id: 'req_15', - parentId: workspace._id, - url: 'http://unix:3000/requestA', - method: 'GET', - }); - const request2 = Object.assign(models.request.init(), { - _id: 'req_10', - parentId: workspace._id, - url: 'http://unix:3000/requestB', - method: 'GET', - }); - const renderedRequest1 = await getRenderedRequest({ request: request1 }); - const renderedRequest2 = await getRenderedRequest({ request: request2 }); - - // WHEN - const response1Promise = networkUtils._actuallySend( - renderedRequest1, - workspace, - settings, - ); - - const response2Promise = networkUtils._actuallySend( - renderedRequest2, - workspace, - settings, - ); - - await networkUtils.cancelRequestById(renderedRequest1._id); - const response1 = await response1Promise; - const response2 = await response2Promise; - // THEN - expect(response1.statusMessage).toBe('Cancelled'); - expect(response2.statusMessage).toBe('OK'); - expect(networkUtils.hasCancelFunctionForId(request1._id)).toBe(false); - expect(networkUtils.hasCancelFunctionForId(request2._id)).toBe(false); - }); }); describe('_getAwsAuthHeaders', () => { @@ -888,7 +848,7 @@ describe('_parseHeaders', () => { const minimalHeaders = ['HTTP/1.1 301', '']; it('Parses single response headers', () => { - expect(networkUtils._parseHeaders(Buffer.from(basicHeaders.join('\n')))).toEqual([ + expect(_parseHeaders(Buffer.from(basicHeaders.join('\n')))).toEqual([ { code: 301, version: 'HTTP/1.1', @@ -936,7 +896,7 @@ describe('_parseHeaders', () => { }); it('Parses Windows newlines', () => { - expect(networkUtils._parseHeaders(Buffer.from(basicHeaders.join('\r\n')))).toEqual([ + expect(_parseHeaders(Buffer.from(basicHeaders.join('\r\n')))).toEqual([ { code: 301, version: 'HTTP/1.1', @@ -985,7 +945,7 @@ describe('_parseHeaders', () => { it('Parses multiple responses', () => { const blobs = basicHeaders.join('\r\n') + '\n' + minimalHeaders.join('\n'); - expect(networkUtils._parseHeaders(Buffer.from(blobs))).toEqual([ + expect(_parseHeaders(Buffer.from(blobs))).toEqual([ { code: 301, version: 'HTTP/1.1', diff --git a/packages/insomnia-app/app/network/libcurl-promise.ts b/packages/insomnia-app/app/network/libcurl-promise.ts new file mode 100644 index 000000000..af22737e6 --- /dev/null +++ b/packages/insomnia-app/app/network/libcurl-promise.ts @@ -0,0 +1,241 @@ +// 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 { Curl, CurlCode, CurlFeature, CurlInfoDebug } from '@getinsomnia/node-libcurl'; +import fs from 'fs'; +import { Readable, Writable } from 'stream'; +import { ValueOf } from 'type-fest'; + +import { describeByteSize } from '../common/misc'; +import { ResponseHeader } from '../models/response'; +import { ResponsePatch } from './network'; + +// wraps libcurl with a promise taking curl options and others required by read, write and debug callbacks +// returning a response patch, debug timeline and list of headers for each redirect + +interface CurlOpt { + key: Parameters[0]; + value: Parameters[1]; +} + +interface CurlRequestOptions { + curlOptions: CurlOpt[]; + responseBodyPath: string; + maxTimelineDataSizeKB: number; + requestId: string; // for cancellation + requestBodyPath?: string; // only used for POST file path + isMultipart: boolean; // for clean up after implemention side effect +} + +interface ResponseTimelineEntry { + name: ValueOf; + timestamp: number; + value: string; +} + +interface CurlRequestOutput { + patch: ResponsePatch; + debugTimeline: ResponseTimelineEntry[]; + headerResults: HeaderResult[]; +} + +// NOTE: this is a dictionary of functions to close open listeners +const cancelCurlRequestHandlers = {}; +export const cancelCurlRequest = id => cancelCurlRequestHandlers[id](); +export const curlRequest = (options: CurlRequestOptions) => new Promise(async resolve => { + try { + // Create instance and handlers, poke value options in, set up write and debug callbacks, listen for events + const { curlOptions, responseBodyPath, requestBodyPath, maxTimelineDataSizeKB, requestId, isMultipart } = options; + const curl = new Curl(); + let requestFileDescriptor; + const responseBodyWriteStream = fs.createWriteStream(responseBodyPath); + // cancel request by id map + cancelCurlRequestHandlers[requestId] = () => { + if (requestFileDescriptor && responseBodyPath) { + closeReadFunction(requestFileDescriptor, isMultipart, requestBodyPath); + } + curl.close(); + }; + // set the string and number options from network.ts + curlOptions.forEach(opt => curl.setOpt(opt.key, opt.value)); + // read file into request and close file desriptor + if (requestBodyPath) { + requestFileDescriptor = fs.openSync(requestBodyPath, 'r'); + curl.setOpt(Curl.option.READDATA, requestFileDescriptor); + curl.on('end', () => closeReadFunction(requestFileDescriptor, isMultipart, requestBodyPath)); + curl.on('error', () => closeReadFunction(requestFileDescriptor, isMultipart, requestBodyPath)); + } + + // set up response writer + let responseBodyBytes = 0; + curl.setOpt(Curl.option.WRITEFUNCTION, buffer => { + responseBodyBytes += buffer.length; + responseBodyWriteStream.write(buffer); + return buffer.length; + }); + // set up response logger + const debugTimeline: ResponseTimelineEntry[] = []; + curl.setOpt(Curl.option.DEBUGFUNCTION, (infoType, buffer) => { + const rawName = Object.keys(CurlInfoDebug).find(k => CurlInfoDebug[k] === infoType) || ''; + const infoTypeName = LIBCURL_DEBUG_MIGRATION_MAP[rawName] || rawName; + + const isSSLData = infoType === CurlInfoDebug.SslDataIn || infoType === CurlInfoDebug.SslDataOut; + const isEmpty = buffer.length === 0; + // Don't show cookie setting because this will display every domain in the jar + const isAddCookie = infoType === CurlInfoDebug.Text && buffer.toString('utf8').indexOf('Added cookie') === 0; + if (isSSLData || isEmpty || isAddCookie) { + return 0; + } + + let value; + if (infoType === CurlInfoDebug.DataOut) { + // Ignore the possibly large data messages + const lessThan10KB = buffer.length / 1024 < maxTimelineDataSizeKB || 10; + value = lessThan10KB ? buffer.toString('utf8') : `(${describeByteSize(buffer.length)} hidden)`; + } + if (infoType === CurlInfoDebug.DataIn) { + value = `Received ${describeByteSize(buffer.length)} chunk`; + } + + debugTimeline.push({ + name: infoType === CurlInfoDebug.DataIn ? 'TEXT' : infoTypeName, + value: value || buffer.toString('utf8'), + timestamp: Date.now(), + }); + return 0; // Must be here + }); + + // makes rawHeaders a buffer, rather than HeaderInfo[] + curl.enable(CurlFeature.Raw); + // NOTE: legacy write end callback + curl.on('end', () => responseBodyWriteStream.end()); + curl.on('end', async (_1, _2, rawHeaders: Buffer) => { + const patch = { + bytesContent: responseBodyBytes, + bytesRead: curl.getInfo(Curl.info.SIZE_DOWNLOAD) as number, + elapsedTime: curl.getInfo(Curl.info.TOTAL_TIME) as number * 1000, + url: curl.getInfo(Curl.info.EFFECTIVE_URL) as string, + }; + curl.close(); + await waitForStreamToFinish(responseBodyWriteStream); + + const headerResults = _parseHeaders(rawHeaders); + resolve({ patch, debugTimeline, headerResults }); + }); + // NOTE: legacy write end callback + curl.on('error', () => responseBodyWriteStream.end()); + curl.on('error', async function(err, code) { + const elapsedTime = curl.getInfo(Curl.info.TOTAL_TIME) as number * 1000; + curl.close(); + await waitForStreamToFinish(responseBodyWriteStream); + + let error = err + ''; + let statusMessage = 'Error'; + + if (code === CurlCode.CURLE_ABORTED_BY_CALLBACK) { + error = 'Request aborted'; + statusMessage = 'Abort'; + } + const patch = { + statusMessage, + error: error || 'Something went wrong', + elapsedTime, + }; + + // NOTE: legacy, default headerResults + resolve({ patch, debugTimeline, headerResults: [{ version: '', code: -1, reason: '', headers: [] }] }); + }); + curl.perform(); + } catch (e) { + const patch = { + statusMessage: 'Error', + error: e.message || 'Something went wrong', + elapsedTime: 0, + }; + resolve({ patch, debugTimeline: [], headerResults: [{ version: '', code: -1, reason: '', headers: [] }] }); + } +}); + +const closeReadFunction = (fd: number, isMultipart: boolean, path?: string) => { + fs.closeSync(fd); + // NOTE: multipart files are combined before sending, so this file is deleted after + // alt implemention to send one part at a time https://github.com/JCMais/node-libcurl/blob/develop/examples/04-multi.js + if (isMultipart && path) fs.unlink(path, () => { }); +}; + +// Because node-libcurl changed some names that we used in the timeline +const LIBCURL_DEBUG_MIGRATION_MAP = { + HeaderIn: 'HEADER_IN', + DataIn: 'DATA_IN', + SslDataIn: 'SSL_DATA_IN', + HeaderOut: 'HEADER_OUT', + DataOut: 'DATA_OUT', + SslDataOut: 'SSL_DATA_OUT', + Text: 'TEXT', + '': '', +}; + +interface HeaderResult { + headers: ResponseHeader[]; + version: string; + code: number; + reason: string; +} +// NOTE: legacy, has tests, could be simplified +export function _parseHeaders(buffer: Buffer) { + const results: HeaderResult[] = []; + const lines = buffer.toString('utf8').split(/\r?\n|\r/g); + + for (let i = 0, currentResult: HeaderResult | null = null; i < lines.length; i++) { + const line = lines[i]; + const isEmptyLine = line.trim() === ''; + + // If we hit an empty line, start parsing the next response + if (isEmptyLine && currentResult) { + results.push(currentResult); + currentResult = null; + continue; + } + + if (!currentResult) { + const [version, code, ...other] = line.split(/ +/g); + currentResult = { + version, + code: parseInt(code, 10), + reason: other.join(' '), + headers: [], + }; + } else { + const [name, value] = line.split(/:\s(.+)/); + const header: ResponseHeader = { + name, + value: value || '', + }; + currentResult.headers.push(header); + } + } + + return results; +} +// NOTE: legacy, suspicious, could be simplified +async function waitForStreamToFinish(stream: Readable | Writable) { + return new Promise(resolve => { + // @ts-expect-error -- access of internal values that are intended to be private. We should _not_ do this. + if (stream._readableState?.finished) { + return resolve(); + } + + // @ts-expect-error -- access of internal values that are intended to be private. We should _not_ do this. + if (stream._writableState?.finished) { + return resolve(); + } + + stream.on('close', () => { + resolve(); + }); + stream.on('error', () => { + resolve(); + }); + }); +} diff --git a/packages/insomnia-app/app/network/network.ts b/packages/insomnia-app/app/network/network.ts index 0c66c3dbc..9c2905a4d 100644 --- a/packages/insomnia-app/app/network/network.ts +++ b/packages/insomnia-app/app/network/network.ts @@ -1,15 +1,8 @@ -import { - Curl, - CurlAuth, - CurlCode, - CurlFeature, - CurlHttpVersion, - CurlInfoDebug, - CurlNetrc, -} from '@getinsomnia/node-libcurl'; +import { CurlAuth } from '@getinsomnia/node-libcurl/dist/enum/CurlAuth'; +import { CurlHttpVersion } from '@getinsomnia/node-libcurl/dist/enum/CurlHttpVersion'; +import { CurlNetrc } from '@getinsomnia/node-libcurl/dist/enum/CurlNetrc'; import aws4 from 'aws4'; import clone from 'clone'; -import crypto from 'crypto'; import fs from 'fs'; import { HttpVersions } from 'insomnia-common'; import { cookiesFromJar, jarFromCookies } from 'insomnia-cookies'; @@ -38,7 +31,6 @@ import { database as db } from '../common/database'; import { getDataDirectory, getTempDir } from '../common/electron-helpers'; import { delay, - describeByteSize, getContentTypeHeader, getHostHeader, getLocationHeader, @@ -49,7 +41,6 @@ import { hasContentTypeHeader, hasUserAgentHeader, LIBCURL_DEBUG_MIGRATION_MAP, - waitForStreamToFinish, } from '../common/misc'; import type { ExtraRenderInfo, RenderedRequest } from '../common/render'; import { @@ -70,6 +61,48 @@ import caCerts from './ca-certs'; import { buildMultipart } from './multipart'; import { urlMatchesCertHost } from './url-matches-cert-host'; +// Based on list of option properties but with callback options removed +const Curl = { + option: { + ACCEPT_ENCODING: 'ACCEPT_ENCODING', + CAINFO: 'CAINFO', + COOKIE: 'COOKIE', + COOKIEFILE: 'COOKIEFILE', + COOKIELIST: 'COOKIELIST', + CUSTOMREQUEST: 'CUSTOMREQUEST', + FOLLOWLOCATION: 'FOLLOWLOCATION', + HTTPAUTH: 'HTTPAUTH', + HTTPGET: 'HTTPGET', + HTTPHEADER: 'HTTPHEADER', + HTTPPOST: 'HTTPPOST', + HTTP_VERSION: 'HTTP_VERSION', + INFILESIZE_LARGE: 'INFILESIZE_LARGE', + KEYPASSWD: 'KEYPASSWD', + MAXREDIRS: 'MAXREDIRS', + NETRC: 'NETRC', + NOBODY: 'NOBODY', + NOPROGRESS: 'NOPROGRESS', + NOPROXY: 'NOPROXY', + PASSWORD: 'PASSWORD', + POST: 'POST', + POSTFIELDS: 'POSTFIELDS', + PATH_AS_IS: 'PATH_AS_IS', + PROXY: 'PROXY', + PROXYAUTH: 'PROXYAUTH', + SSLCERT: 'SSLCERT', + SSLCERTTYPE: 'SSLCERTTYPE', + SSLKEY: 'SSLKEY', + SSL_VERIFYHOST: 'SSL_VERIFYHOST', + SSL_VERIFYPEER: 'SSL_VERIFYPEER', + TIMEOUT_MS: 'TIMEOUT_MS', + UNIX_SOCKET_PATH: 'UNIX_SOCKET_PATH', + UPLOAD: 'UPLOAD', + URL: 'URL', + USERAGENT: 'USERAGENT', + USERNAME: 'USERNAME', + VERBOSE: 'VERBOSE', + }, +}; export interface ResponsePatch { bodyCompression?: 'zip' | null; bodyPath?: string; @@ -121,27 +154,13 @@ export const getHttpVersion = preferredHttpVersion => { }; export async function cancelRequestById(requestId) { - if (hasCancelFunctionForId(requestId)) { - const cancelRequestFunction = cancelRequestFunctionMap[requestId]; - - if (typeof cancelRequestFunction === 'function') { - return cancelRequestFunction(); - } + 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`); } -function clearCancelFunctionForId(requestId) { - if (hasCancelFunctionForId(requestId)) { - delete cancelRequestFunctionMap[requestId]; - } -} - -export function hasCancelFunctionForId(requestId) { - return cancelRequestFunctionMap.hasOwnProperty(requestId); -} - export async function _actuallySend( renderedRequest: RenderedRequest, workspace: Workspace, @@ -162,17 +181,17 @@ export async function _actuallySend( const addTimelineText = addTimelineItem(LIBCURL_DEBUG_MIGRATION_MAP.Text); - // Initialize the curl handle - const curl = new Curl(); - /** Helper function to respond with a success */ async function respond( patch: ResponsePatch, bodyPath: string | null, + debugTimeline: any[] = [] ) { - const timelinePath = await storeTimeline(timeline); + const timelinePath = await storeTimeline([...timeline, ...debugTimeline]); // Tear Down the cancellation logic - clearCancelFunctionForId(renderedRequest._id); + if (cancelRequestFunctionMap.hasOwnProperty(renderedRequest._id)) { + delete cancelRequestFunctionMap[renderedRequest._id]; + } const environmentId = environment ? environment._id : null; return resolve(Object.assign( { @@ -191,6 +210,7 @@ export async function _actuallySend( /** Helper function to respond with an error */ async function handleError(err: Error) { + await respond( { url: renderedRequest.url, @@ -204,34 +224,32 @@ export async function _actuallySend( null, ); } - - /** Helper function to set Curl options */ - const setOpt: typeof curl.setOpt = (opt: any, val: any) => { - try { - return curl.setOpt(opt, val); - } catch (err) { - const name = Object.keys(Curl.option).find(name => Curl.option[name] === opt); - throw new Error(`${err.message} (${opt} ${name || 'n/a'})`); - } + // NOTE: can have duplicate keys because of cookie options + const curlOptions: { key: string; value: string | string[] | number | boolean }[] = []; + const setOpt = (key: string, value: string | string[] | number | boolean) => { + curlOptions.push({ key, value }); }; try { // Setup the cancellation logic cancelRequestFunctionMap[renderedRequest._id] = async () => { + await respond( { - elapsedTime: (curl.getInfo(Curl.info.TOTAL_TIME) as number || 0) * 1000, - // @ts-expect-error -- needs generic - bytesRead: curl.getInfo(Curl.info.SIZE_DOWNLOAD), - // @ts-expect-error -- needs generic - url: curl.getInfo(Curl.info.EFFECTIVE_URL), + elapsedTime: 0, + bytesRead: 0, + url: renderedRequest.url, statusMessage: 'Cancelled', error: 'Request was cancelled', }, null, ); - // Kill it! - curl.close(); + // NOTE: conditionally use ipc bridge, renderer cannot import native modules directly + const nodejsCancelCurlRequest = process.type === 'renderer' + ? window.main.cancelCurlRequest + // eslint-disable-next-line @typescript-eslint/no-var-requires + : require('./libcurl-promise').cancelCurlRequest; + nodejsCancelCurlRequest(renderedRequest._id); }; // Set all the basic options @@ -243,9 +261,6 @@ export async function _actuallySend( // True so curl doesn't print progress setOpt(Curl.option.ACCEPT_ENCODING, ''); - // Auto decode everything - curl.enable(CurlFeature.Raw); - // Set follow redirects setting switch (renderedRequest.settingFollowRedirects) { case 'off': @@ -292,44 +307,6 @@ export async function _actuallySend( break; } - // Setup debug handler - setOpt(Curl.option.DEBUGFUNCTION, (infoType, contentBuffer) => { - const content = contentBuffer.toString('utf8'); - const rawName = Object.keys(CurlInfoDebug).find(k => CurlInfoDebug[k] === infoType) || ''; - const name = LIBCURL_DEBUG_MIGRATION_MAP[rawName] || rawName; - const addToTimeline = addTimelineItem(name); - - if (infoType === CurlInfoDebug.SslDataIn || infoType === CurlInfoDebug.SslDataOut) { - return 0; - } - - // Ignore the possibly large data messages - if (infoType === CurlInfoDebug.DataOut) { - if (contentBuffer.length === 0) { - // Sometimes this happens, but I'm not sure why. Just ignore it. - } else if (contentBuffer.length / 1024 < settings.maxTimelineDataSizeKB) { - addToTimeline(content); - } else { - addToTimeline(`(${describeByteSize(contentBuffer.length)} hidden)`); - } - - return 0; - } - - if (infoType === CurlInfoDebug.DataIn) { - addTimelineText(`Received ${describeByteSize(contentBuffer.length)} chunk`); - return 0; - } - - // Don't show cookie setting because this will display every domain in the jar - if (infoType === CurlInfoDebug.Text && content.indexOf('Added cookie') === 0) { - return 0; - } - - addToTimeline(content); - return 0; // Must be here - }); - // Set the headers (to be modified as we go) const headers = clone(renderedRequest.headers); // Set the URL, including the query parameters @@ -352,7 +329,6 @@ export async function _actuallySend( addTimelineText('Preparing request to ' + finalUrl); addTimelineText('Current time is ' + new Date().toISOString()); - addTimelineText(`Using ${Curl.getVersion()}`); const httpVersion = getHttpVersion(settings.preferredHttpVersion); addTimelineText(httpVersion.log); @@ -523,7 +499,8 @@ export async function _actuallySend( let noBody = false; let requestBody: string | null = null; const expectsBody = ['POST', 'PUT', 'PATCH'].includes(renderedRequest.method.toUpperCase()); - + let requestBodyPath; + let isMultipart = false; if (renderedRequest.body.mimeType === CONTENT_TYPE_FORM_URLENCODED) { requestBody = buildQueryStringFromParams(renderedRequest.body.params || [], false); } else if (renderedRequest.body.mimeType === CONTENT_TYPE_FORM_DATA) { @@ -531,6 +508,8 @@ export async function _actuallySend( const { filePath: multipartBodyPath, boundary, contentLength } = await buildMultipart( params, ); + requestBodyPath = multipartBodyPath; + isMultipart = true; // Extend the Content-Type header const contentTypeHeader = getContentTypeHeader(headers); @@ -543,36 +522,18 @@ export async function _actuallySend( }); } - const fd = fs.openSync(multipartBodyPath, 'r'); setOpt(Curl.option.INFILESIZE_LARGE, contentLength); setOpt(Curl.option.UPLOAD, 1); - setOpt(Curl.option.READDATA, fd); // We need this, otherwise curl will send it as a PUT setOpt(Curl.option.CUSTOMREQUEST, renderedRequest.method); - - const fn = () => { - fs.closeSync(fd); - fs.unlink(multipartBodyPath, () => { - // Pass - }); - }; - - curl.on('end', fn); - curl.on('error', fn); } else if (renderedRequest.body.fileName) { const { size } = fs.statSync(renderedRequest.body.fileName); - const fileName = renderedRequest.body.fileName || ''; - const fd = fs.openSync(fileName, 'r'); + requestBodyPath = renderedRequest.body.fileName || ''; + setOpt(Curl.option.INFILESIZE_LARGE, size); setOpt(Curl.option.UPLOAD, 1); - setOpt(Curl.option.READDATA, fd); // We need this, otherwise curl will send it as a POST setOpt(Curl.option.CUSTOMREQUEST, renderedRequest.method); - - const fn = () => fs.closeSync(fd); - - curl.on('end', fn); - curl.on('error', fn); } else if (typeof renderedRequest.body.mimeType === 'string' || expectsBody) { requestBody = renderedRequest.body.text || ''; } else { @@ -697,118 +658,86 @@ export async function _actuallySend( } }); setOpt(Curl.option.HTTPHEADER, headerStrings); - let responseBodyBytes = 0; + const responsesDir = pathJoin(getDataDirectory(), 'responses'); mkdirp.sync(responsesDir); const responseBodyPath = pathJoin(responsesDir, uuid.v4() + '.response'); - const responseBodyWriteStream = fs.createWriteStream(responseBodyPath); - curl.on('end', () => responseBodyWriteStream.end()); - curl.on('error', () => responseBodyWriteStream.end()); - setOpt(Curl.option.WRITEFUNCTION, buff => { - responseBodyBytes += buff.length; - responseBodyWriteStream.write(buff); - return buff.length; - }); - // Handle the response ending - curl.on('end', async (_1, _2, rawHeaders: Buffer) => { - const allCurlHeadersObjects = _parseHeaders(rawHeaders); + // NOTE: conditionally use ipc bridge, renderer cannot import native modules directly + const nodejsCurlRequest = process.type === 'renderer' + ? window.main.curlRequest + // eslint-disable-next-line @typescript-eslint/no-var-requires + : require('./libcurl-promise').curlRequest; + const requestOptions = { + curlOptions, + responseBodyPath, + requestBodyPath, + isMultipart, + maxTimelineDataSizeKB: settings.maxTimelineDataSizeKB, + requestId: renderedRequest._id, + }; + const { patch, debugTimeline, headerResults } = await nodejsCurlRequest(requestOptions); - // Headers are an array (one for each redirect) - const lastCurlHeadersObject = allCurlHeadersObjects[allCurlHeadersObjects.length - 1]; - // Collect various things - const httpVersion = lastCurlHeadersObject.version || ''; - const statusCode = lastCurlHeadersObject.code || -1; - const statusMessage = lastCurlHeadersObject.reason || ''; - // Collect the headers - const headers = lastCurlHeadersObject.headers; - // Calculate the content type - const contentTypeHeader = getContentTypeHeader(headers); - const contentType = contentTypeHeader ? contentTypeHeader.value : ''; - // Update Cookie Jar - let currentUrl = finalUrl; - let setCookieStrings: string[] = []; - const jar = jarFromCookies(renderedRequest.cookieJar.cookies); + // Headers are an array (one for each redirect) + const lastCurlHeadersObject = headerResults[headerResults.length - 1]; - for (const { headers } of allCurlHeadersObjects) { - // Collect Set-Cookie headers - const setCookieHeaders = getSetCookieHeaders(headers); - setCookieStrings = [...setCookieStrings, ...setCookieHeaders.map(h => h.value)]; - // Pull out new URL if there is a redirect - const newLocation = getLocationHeader(headers); + // Calculate the content type + const contentTypeHeader = getContentTypeHeader(lastCurlHeadersObject.headers); + // Update Cookie Jar + let currentUrl = finalUrl; + let setCookieStrings: string[] = []; + const jar = jarFromCookies(renderedRequest.cookieJar.cookies); - if (newLocation !== null) { - currentUrl = urlResolve(currentUrl, newLocation.value); - } + for (const { headers } of headerResults) { + // Collect Set-Cookie headers + const setCookieHeaders = getSetCookieHeaders(headers); + setCookieStrings = [...setCookieStrings, ...setCookieHeaders.map(h => h.value)]; + // Pull out new URL if there is a redirect + const newLocation = getLocationHeader(headers); + + if (newLocation !== null) { + currentUrl = urlResolve(currentUrl, newLocation.value); } + } - // Update jar with Set-Cookie headers - for (const setCookieStr of setCookieStrings) { - try { - jar.setCookieSync(setCookieStr, currentUrl); - } catch (err) { - addTimelineText(`Rejected cookie: ${err.message}`); - } + // Update jar with Set-Cookie headers + for (const setCookieStr of setCookieStrings) { + try { + jar.setCookieSync(setCookieStr, currentUrl); + } catch (err) { + addTimelineText(`Rejected cookie: ${err.message}`); } + } - // Update cookie jar if we need to and if we found any cookies - if (renderedRequest.settingStoreCookies && setCookieStrings.length) { - const cookies = await cookiesFromJar(jar); - await models.cookieJar.update(renderedRequest.cookieJar, { - cookies, - }); + // Update cookie jar if we need to and if we found any cookies + if (renderedRequest.settingStoreCookies && setCookieStrings.length) { + const cookies = await cookiesFromJar(jar); + await models.cookieJar.update(renderedRequest.cookieJar, { + cookies, + }); + } + + // Print informational message + if (setCookieStrings.length > 0) { + const n = setCookieStrings.length; + + if (renderedRequest.settingStoreCookies) { + addTimelineText(`Saved ${n} cookie${n === 1 ? '' : 's'}`); + } else { + addTimelineText(`Ignored ${n} cookie${n === 1 ? '' : 's'}`); } + } - // Print informational message - if (setCookieStrings.length > 0) { - const n = setCookieStrings.length; + const responsePatch: ResponsePatch = { + contentType: contentTypeHeader ? contentTypeHeader.value : '', + headers: lastCurlHeadersObject.headers, + httpVersion: lastCurlHeadersObject.version, + statusCode: lastCurlHeadersObject.code, + statusMessage: lastCurlHeadersObject.reason, + ...patch, + }; - if (renderedRequest.settingStoreCookies) { - addTimelineText(`Saved ${n} cookie${n === 1 ? '' : 's'}`); - } else { - addTimelineText(`Ignored ${n} cookie${n === 1 ? '' : 's'}`); - } - } + respond(responsePatch, responseBodyPath, debugTimeline); - // Return the response data - const responsePatch: ResponsePatch = { - contentType, - headers, - httpVersion, - statusCode, - statusMessage, - bytesContent: responseBodyBytes, - // @ts-expect-error -- TSCONVERSION appears to be a genuine error - bytesRead: curl.getInfo(Curl.info.SIZE_DOWNLOAD), - elapsedTime: curl.getInfo(Curl.info.TOTAL_TIME) as number * 1000, - // @ts-expect-error -- TSCONVERSION appears to be a genuine error - url: curl.getInfo(Curl.info.EFFECTIVE_URL), - }; - // Close the request - curl.close(); - // Make sure the response body has been fully written first - await waitForStreamToFinish(responseBodyWriteStream); - // Send response - await respond(responsePatch, responseBodyPath); - }); - curl.on('error', async function(err, code) { - let error = err + ''; - let statusMessage = 'Error'; - - if (code === CurlCode.CURLE_ABORTED_BY_CALLBACK) { - error = 'Request aborted'; - statusMessage = 'Abort'; - } - - await respond( - { - statusMessage, - error: error || 'Something went wrong', - elapsedTime: curl.getInfo(Curl.info.TOTAL_TIME) as number * 1000, - }, - null, - ); - }); - curl.perform(); } catch (err) { console.log('[network] Error', err); await handleError(err); @@ -820,12 +749,12 @@ 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, @@ -851,7 +780,6 @@ export async function sendWithSettings( request: RenderedRequest; context: Record; }; - try { renderResult = await getRenderedRequestAndContext({ request: newRequest, environmentId }); } catch (err) { @@ -892,12 +820,12 @@ export async function send( */ 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, [ @@ -919,6 +847,7 @@ export async function send( extraInfo, }, ); + const renderedRequestBeforePlugins = renderResult.request; const renderedContextBeforePlugins = renderResult.context; const workspaceDoc = ancestors.find(isWorkspace); @@ -931,6 +860,7 @@ export async function send( let renderedRequest: RenderedRequest; try { + console.log('[network] Apply plugin pre hooks'); renderedRequest = await _applyRequestPluginHooks( renderedRequestBeforePlugins, renderedContextBeforePlugins, @@ -955,6 +885,7 @@ export async function send( environment, settings.validateSSL, ); + console.log( response.error ? `[network] Response failed req=${requestId} err=${response.error || 'n/a'}` @@ -1038,51 +969,6 @@ async function _applyResponsePluginHooks( } -interface HeaderResult { - headers: ResponseHeader[]; - version: string; - code: number; - reason: string; -} - -export function _parseHeaders( - buffer: Buffer, -) { - const results: HeaderResult[] = []; - const lines = buffer.toString('utf8').split(/\r?\n|\r/g); - - for (let i = 0, currentResult: HeaderResult | null = null; i < lines.length; i++) { - const line = lines[i]; - const isEmptyLine = line.trim() === ''; - - // If we hit an empty line, start parsing the next response - if (isEmptyLine && currentResult) { - results.push(currentResult); - currentResult = null; - continue; - } - - if (!currentResult) { - const [version, code, ...other] = line.split(/ +/g); - currentResult = { - version, - code: parseInt(code, 10), - reason: other.join(' '), - headers: [], - }; - } else { - const [name, value] = line.split(/:\s(.+)/); - const header: ResponseHeader = { - name, - value: value || '', - }; - currentResult.headers.push(header); - } - } - - return results; -} - // exported for unit tests only export function _getAwsAuthHeaders( credentials: { @@ -1130,18 +1016,20 @@ export function _getAwsAuthHeaders( } function storeTimeline(timeline: ResponseTimelineEntry[]) { + const timelineStr = JSON.stringify(timeline, null, '\t'); + const timelineHash = uuid.v4(); + const responsesDir = pathJoin(getDataDirectory(), 'responses'); + mkdirp.sync(responsesDir); + const timelinePath = pathJoin(responsesDir, timelineHash + '.timeline'); + if (process.type === 'renderer'){ + return window.main.writeFile({ path: timelinePath, content: timelineStr }); + } return new Promise((resolve, reject) => { - const timelineStr = JSON.stringify(timeline, null, '\t'); - const timelineHash = crypto.createHash('sha1').update(timelineStr).digest('hex'); - const responsesDir = pathJoin(getDataDirectory(), 'responses'); - mkdirp.sync(responsesDir); - const timelinePath = pathJoin(responsesDir, timelineHash + '.timeline'); fs.writeFile(timelinePath, timelineStr, err => { if (err != null) { - reject(err); - } else { - resolve(timelinePath); + return reject(err); } + resolve(timelinePath); }); }); } diff --git a/packages/insomnia-app/app/preload.js b/packages/insomnia-app/app/preload.js index a78004551..c893ca4f8 100644 --- a/packages/insomnia-app/app/preload.js +++ b/packages/insomnia-app/app/preload.js @@ -5,6 +5,9 @@ const main = { authorizeUserInWindow: options => ipcRenderer.invoke('authorizeUserInWindow', options), setMenuBarVisibility: options => ipcRenderer.send('setMenuBarVisibility', options), installPlugin: options => ipcRenderer.invoke('installPlugin', options), + curlRequest: options => ipcRenderer.invoke('curlRequest', options), + cancelCurlRequest: options => ipcRenderer.send('cancelCurlRequest', options), + writeFile: options => ipcRenderer.invoke('writeFile', options), }; const dialog = { showOpenDialog: options => ipcRenderer.invoke('showOpenDialog', options), diff --git a/packages/insomnia-app/app/templating/index.ts b/packages/insomnia-app/app/templating/index.ts index fd8a12c9f..67fa36676 100644 --- a/packages/insomnia-app/app/templating/index.ts +++ b/packages/insomnia-app/app/templating/index.ts @@ -44,6 +44,10 @@ export function render( renderMode?: string; } = {}, ) { + const hasNunjucksInterpolationSymbols = text.includes('{{') && text.includes('}}'); + const hasNunjucksCustomTagSymbols = text.includes('{%') && text.includes('%}'); + const hasNunjucksCommentSymbols = text.includes('{#') && text.includes('#}'); + if (!hasNunjucksInterpolationSymbols && !hasNunjucksCustomTagSymbols && !hasNunjucksCommentSymbols) return text; const context = config.context || {}; // context needs to exist on the root for the old templating syntax, and in _ for the new templating syntax // old: {{ arr[0].prop }} @@ -52,9 +56,13 @@ export function render( const path = config.path || null; const renderMode = config.renderMode || RENDER_ALL; return new Promise(async (resolve, reject) => { + // NOTE: this is added as a breadcrumb because renderString sometimes hangs + const id = setTimeout(() => console.log('Warning: nunjucks failed to respond within 5 seconds'), 5000); const nj = await getNunjucks(renderMode); nj?.renderString(text, templatingContext, (err, result) => { + clearTimeout(id); if (err) { + console.log(err); const sanitizedMsg = err.message .replace(/\(unknown path\)\s/, '') .replace(/\[Line \d+, Column \d*]/, '') diff --git a/packages/insomnia-app/app/ui/components/modals/settings-modal.tsx b/packages/insomnia-app/app/ui/components/modals/settings-modal.tsx index bd8ea6d62..d680adb29 100644 --- a/packages/insomnia-app/app/ui/components/modals/settings-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/settings-modal.tsx @@ -1,4 +1,3 @@ -import { Curl } from '@getinsomnia/node-libcurl'; import { autoBindMethodsForReact } from 'class-autobind-decorator'; import { HotKeyRegistry } from 'insomnia-common'; import React, { PureComponent } from 'react'; @@ -20,7 +19,6 @@ import { ImportExport } from '../settings/import-export'; import { Plugins } from '../settings/plugins'; import { Shortcuts } from '../settings/shortcuts'; import { ThemePanel } from '../settings/theme-panel'; -import { Tooltip } from '../tooltip'; import { showModal } from './index'; export const TAB_INDEX_EXPORT = 1; @@ -80,9 +78,6 @@ export class UnconnectedSettingsModal extends PureComponent { {getAppName()} Preferences   –  v{getAppVersion()} - - - {email ? ` – ${email}` : null} diff --git a/packages/insomnia-app/app/ui/containers/app.tsx b/packages/insomnia-app/app/ui/containers/app.tsx index 06c67d28c..3e9b6e534 100644 --- a/packages/insomnia-app/app/ui/containers/app.tsx +++ b/packages/insomnia-app/app/ui/containers/app.tsx @@ -806,6 +806,7 @@ class App extends PureComponent { try { const responsePatch = await network.send(requestId, environmentId); await models.response.create(responsePatch, settings.maxHistoryResponses); + } catch (err) { if (err.type === 'render') { showModal(RequestRenderErrorModal, { @@ -831,6 +832,7 @@ class App extends PureComponent { await updateRequestMetaByParentId(requestId, { activeResponseId: null, }); + // Stop loading handleStopLoading(requestId); } diff --git a/packages/insomnia-inso/src/jest/setup.ts b/packages/insomnia-inso/src/jest/setup.ts index e35f350b5..0a0f2f2f5 100644 --- a/packages/insomnia-inso/src/jest/setup.ts +++ b/packages/insomnia-inso/src/jest/setup.ts @@ -1,2 +1 @@ -jest.mock('@getinsomnia/node-libcurl'); process.env.DEFAULT_APP_NAME = process.env.DEFAULT_APP_NAME || 'insomnia-app'; diff --git a/packages/insomnia-smoke-test/cli/app.test.ts b/packages/insomnia-smoke-test/cli/app.test.ts index f2bc95962..636316856 100644 --- a/packages/insomnia-smoke-test/cli/app.test.ts +++ b/packages/insomnia-smoke-test/cli/app.test.ts @@ -34,6 +34,7 @@ describe.each(compact([npmPackageBinPath, ...binaries]))('inso with %s', binPath srcInsoNedb, ['--env', 'Dev'], 'Echo Test Suite', + '--verbose', ); expect(failed).toBe(false); diff --git a/packages/insomnia-smoke-test/fixtures/oauth.yaml b/packages/insomnia-smoke-test/fixtures/oauth.yaml index 7d40649da..876b8dcb6 100644 --- a/packages/insomnia-smoke-test/fixtures/oauth.yaml +++ b/packages/insomnia-smoke-test/fixtures/oauth.yaml @@ -7,7 +7,7 @@ resources: parentId: fld_0e50ba4426bb4540ade91e0525ea1f29 modified: 1645664215605 created: 1645544268127 - url: "{{ _.oidc_base_path }}/me" + url: "http://127.0.0.1:4010/oidc/me" name: No PKCE description: "" method: GET @@ -15,13 +15,13 @@ resources: parameters: [] headers: [] authentication: - accessTokenUrl: "{{ _.oidc_base_path }}/token" - authorizationUrl: "{{ _.oidc_base_path }}/auth" - clientId: "{{ _.client_id_authorization_code }}" - clientSecret: "{{ _.client_secret }}" + accessTokenUrl: "http://127.0.0.1:4010/oidc/token" + authorizationUrl: "http://127.0.0.1:4010/oidc/auth" + clientId: "authorization_code" + clientSecret: "secret" disabled: false grantType: authorization_code - redirectUrl: "{{ _.oidc_callback }}" + redirectUrl: "http://127.0.0.1:4010/callback" responseType: id_token scope: openid offline_access state: "" @@ -60,7 +60,7 @@ resources: parentId: fld_0e50ba4426bb4540ade91e0525ea1f29 modified: 1645664217727 created: 1645220819802 - url: "{{ _.oidc_base_path }}/me" + url: "http://127.0.0.1:4010/oidc/me" name: PKCE SHA256 description: "" method: GET @@ -68,13 +68,13 @@ resources: parameters: [] headers: [] authentication: - accessTokenUrl: "{{ _.oidc_base_path }}/token" - authorizationUrl: "{{ _.oidc_base_path }}/auth" - clientId: "{{ _.client_id_authorization_code_pkce }}" - clientSecret: "{{ _.client_secret }}" + accessTokenUrl: "http://127.0.0.1:4010/oidc/token" + authorizationUrl: "http://127.0.0.1:4010/oidc/auth" + clientId: "authorization_code_pkce" + clientSecret: "secret" disabled: false grantType: authorization_code - redirectUrl: "{{ _.oidc_callback }}" + redirectUrl: "http://127.0.0.1:4010/callback" responseType: id_token scope: openid offline_access state: "" @@ -93,7 +93,7 @@ resources: parentId: fld_0e50ba4426bb4540ade91e0525ea1f29 modified: 1645664218264 created: 1645543526615 - url: "{{ _.oidc_base_path }}/me" + url: "http://127.0.0.1:4010/oidc/me" name: PKCE Plain description: "" method: GET @@ -101,13 +101,13 @@ resources: parameters: [] headers: [] authentication: - accessTokenUrl: "{{ _.oidc_base_path }}/token" - authorizationUrl: "{{ _.oidc_base_path }}/auth" - clientId: "{{ _.client_id_authorization_code_pkce }}" - clientSecret: "{{ _.client_secret }}" + accessTokenUrl: "http://127.0.0.1:4010/oidc/token" + authorizationUrl: "http://127.0.0.1:4010/oidc/auth" + clientId: "authorization_code_pkce" + clientSecret: "secret" disabled: false grantType: authorization_code - redirectUrl: "{{ _.oidc_callback }}" + redirectUrl: "http://127.0.0.1:4010/callback" responseType: id_token scope: openid offline_access state: "" @@ -128,7 +128,7 @@ resources: parentId: fld_d34790add1584643b6688c3add5bbe85 modified: 1645664218947 created: 1645545802379 - url: "{{ _.oidc_base_path }}/id-token" + url: "http://127.0.0.1:4010/oidc/id-token" name: ID Token description: "" method: GET @@ -136,13 +136,13 @@ resources: parameters: [] headers: [] authentication: - accessTokenUrl: "{{ _.oidc_base_path }}/token" - authorizationUrl: "{{ _.oidc_base_path }}/auth" - clientId: "{{ _.client_id_implicit }}" - clientSecret: "{{ _.client_secret }}" + accessTokenUrl: "http://127.0.0.1:4010/oidc/token" + authorizationUrl: "http://127.0.0.1:4010/oidc/auth" + clientId: "implicit" + clientSecret: "secret" disabled: false grantType: implicit - redirectUrl: "{{ _.oidc_callback }}" + redirectUrl: "http://127.0.0.1:4010/callback" responseType: id_token scope: openid state: "" @@ -173,7 +173,7 @@ resources: parentId: fld_d34790add1584643b6688c3add5bbe85 modified: 1645664219446 created: 1645567186775 - url: "{{ _.oidc_base_path }}/me" + url: "http://127.0.0.1:4010/oidc/me" name: ID and Access Token description: "" method: GET @@ -181,13 +181,13 @@ resources: parameters: [] headers: [] authentication: - accessTokenUrl: "{{ _.oidc_base_path }}/token" - authorizationUrl: "{{ _.oidc_base_path }}/auth" - clientId: "{{ _.client_id_implicit }}" - clientSecret: "{{ _.client_secret }}" + accessTokenUrl: "http://127.0.0.1:4010/oidc/token" + authorizationUrl: "http://127.0.0.1:4010/oidc/auth" + clientId: "implicit" + clientSecret: "secret" disabled: false grantType: implicit - redirectUrl: "{{ _.oidc_callback }}" + redirectUrl: "http://127.0.0.1:4010/callback" responseType: id_token token scope: openid state: "" @@ -208,7 +208,7 @@ resources: parentId: wrk_392055e2aa29457b9d2904396cd7631f modified: 1645664219861 created: 1645637343873 - url: "{{ _.oidc_base_path }}/client-credential" + url: "http://127.0.0.1:4010/oidc/client-credential" name: Client Credentials description: "" method: GET @@ -216,13 +216,13 @@ resources: parameters: [] headers: [] authentication: - accessTokenUrl: "{{ _.oidc_base_path }}/token" - authorizationUrl: "{{ _.oidc_base_path }}/auth" - clientId: "{{ _.client_id_client_creds }}" - clientSecret: "{{ _.client_secret }}" + accessTokenUrl: "http://127.0.0.1:4010/oidc/token" + authorizationUrl: "http://127.0.0.1:4010/oidc/auth" + clientId: "client_credentials" + clientSecret: "secret" disabled: false grantType: client_credentials - redirectUrl: "{{ _.oidc_callback }}" + redirectUrl: "http://127.0.0.1:4010/callback" responseType: id_token scope: openid state: "" @@ -245,7 +245,7 @@ resources: parentId: wrk_392055e2aa29457b9d2904396cd7631f modified: 1645664220407 created: 1645636233910 - url: "{{ _.oidc_base_path }}/me" + url: "http://127.0.0.1:4010/oidc/me" name: Resource Owner Password Credentials description: "" method: GET @@ -253,13 +253,13 @@ resources: parameters: [] headers: [] authentication: - accessTokenUrl: "{{ _.oidc_base_path }}/token" - authorizationUrl: "{{ _.oidc_base_path }}/auth" - clientId: "{{ _.client_id_resource_owner }}" - clientSecret: "{{ _.client_secret }}" + accessTokenUrl: "http://127.0.0.1:4010/oidc/token" + authorizationUrl: "http://127.0.0.1:4010/oidc/auth" + clientId: "resource_owner" + clientSecret: "secret" disabled: false grantType: password - redirectUrl: "{{ _.oidc_callback }}" + redirectUrl: "http://127.0.0.1:4010/callback" responseType: id_token scope: openid state: "" @@ -283,27 +283,8 @@ resources: modified: 1645661876119 created: 1645220798237 name: Base Environment - data: - base_url: http://127.0.0.1:4010 - oidc_base_path: "{{ _.base_url }}/oidc" - oidc_callback: "{{ _.base_url }}/callback" - client_id_authorization_code: authorization_code - client_id_authorization_code_pkce: authorization_code_pkce - client_id_implicit: implicit - client_id_client_creds: client_credentials - client_id_resource_owner: resource_owner - client_secret: secret - dataPropertyOrder: - "&": - - base_url - - oidc_base_path - - oidc_callback - - client_id_authorization_code - - client_id_authorization_code_pkce - - client_id_implicit - - client_id_client_creds - - client_id_resource_owner - - client_secret + data: {} + dataPropertyOrder: null color: null isPrivate: false metaSortKey: 1639556944617 diff --git a/packages/insomnia-smoke-test/playwright/test.ts b/packages/insomnia-smoke-test/playwright/test.ts index bce03ddfc..579fc0775 100644 --- a/packages/insomnia-smoke-test/playwright/test.ts +++ b/packages/insomnia-smoke-test/playwright/test.ts @@ -57,7 +57,7 @@ export const test = baseTest.extend<{ page: async ({ app }, use) => { const page = await app.firstWindow(); - if (process.platform === 'win32') await page.reload(); + await page.waitForLoadState(); await page.click("text=Don't share usage analytics"); diff --git a/packages/insomnia-smoke-test/tests/app.test.ts b/packages/insomnia-smoke-test/tests/app.test.ts index 5e0a8db87..b6fecacb1 100644 --- a/packages/insomnia-smoke-test/tests/app.test.ts +++ b/packages/insomnia-smoke-test/tests/app.test.ts @@ -59,7 +59,7 @@ test('can send requests', async ({ app, page }) => { await expect(responseBody).toContainText('Set-Cookie: insomnia-test-cookie=value123'); }); -// This feature is unsafe to place beside other tests, cancelling a request causes node-libcurl to block +// This feature is unsafe to place beside other tests, cancelling a request can cause network code to block // related to https://linear.app/insomnia/issue/INS-973 test('can cancel requests', async ({ app, page }) => { await page.click('[data-testid="project"]'); @@ -73,7 +73,8 @@ test('can cancel requests', async ({ app, page }) => { await page.click('button:has-text("GETdelayed request")'); await page.click('text=http://127.0.0.1:4010/delay/seconds/20Send >> button'); - await page.click('text=Loading...Cancel Request >> button'); + + await page.click('[data-testid="response-pane"] button:has-text("Cancel Request")'); await page.click('text=Request was cancelled'); });