mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 22:30:15 +00:00
electron v15 pre-upgrade refactoring (#4524)
* now with 100% fat free cancellation Co-authored-by: James Gatz <jamesgatzos@gmail.com> * unblock electron 15 * fix cookielist and temp fix curl types * fix types * fix inso * default to verbose inso test * implement readdata function * fix test * revert test changes * isomorphic cancel * reduce typing issues * curl types * turns out the tests were wrong * handle errors * remove unused inso mock * remove request delay * fix lint and add logs * Revert "remove request delay" This reverts commit f07d8c90a7a7279ca10f8a8de1ea0c82caa06390. * simplify and add cancel fallback * skip cancel test * playwright is fast and insomnia is slow * trailing spaces are serious yo * cancel is flake town * hmm * unblock nunjucks and storeTimeline * fix nunjucks tests * preload writeFile * oops forgot to remove the reload * debugging CI takes all day, log stuff and pray * also warn if nunjucks is being lame * Stop using environment variables * revert debugging logs Co-authored-by: James Gatz <jamesgatzos@gmail.com> Co-authored-by: David Marby <david@dmarby.se>
This commit is contained in:
parent
82c37d7f96
commit
8585eea9e6
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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<void>(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<T>(arr: T[], chunkSize: number) {
|
||||
const chunks: T[][] = [];
|
||||
|
||||
|
14
packages/insomnia-app/app/global.d.ts
vendored
14
packages/insomnia-app/app/global.d.ts
vendored
@ -27,6 +27,20 @@ interface Window {
|
||||
authorizeUserInWindow: (options: { url: string; urlSuccessRegex?: RegExp; urlFailureRegex?: RegExp; sessionId: string }) => Promise<string>;
|
||||
setMenuBarVisibility: (visible: boolean) => void;
|
||||
installPlugin: (url: string) => void;
|
||||
writeFile: (options: {path: string; content: string}) => Promise<string>;
|
||||
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<Electron.OpenDialogReturnValue>;
|
||||
|
@ -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<string>((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;
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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',
|
||||
|
241
packages/insomnia-app/app/network/libcurl-promise.ts
Normal file
241
packages/insomnia-app/app/network/libcurl-promise.ts
Normal file
@ -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<Curl['setOpt']>[0];
|
||||
value: Parameters<Curl['setOpt']>[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<typeof LIBCURL_DEBUG_MIGRATION_MAP>;
|
||||
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<CurlRequestOutput>(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<void>(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();
|
||||
});
|
||||
});
|
||||
}
|
@ -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<string, any>,
|
||||
) {
|
||||
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<string, any>;
|
||||
};
|
||||
|
||||
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<string>((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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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<string | null>(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*]/, '')
|
||||
|
@ -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<Props, State> {
|
||||
{getAppName()} Preferences
|
||||
<span className="faint txt-sm">
|
||||
– v{getAppVersion()}
|
||||
<Tooltip position="bottom" message={Curl.getVersion()}>
|
||||
<i className="fa fa-info-circle" />
|
||||
</Tooltip>
|
||||
{email ? ` – ${email}` : null}
|
||||
</span>
|
||||
</ModalHeader>
|
||||
|
@ -806,6 +806,7 @@ class App extends PureComponent<AppProps, State> {
|
||||
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<AppProps, State> {
|
||||
await updateRequestMetaByParentId(requestId, {
|
||||
activeResponseId: null,
|
||||
});
|
||||
|
||||
// Stop loading
|
||||
handleStopLoading(requestId);
|
||||
}
|
||||
|
@ -1,2 +1 @@
|
||||
jest.mock('@getinsomnia/node-libcurl');
|
||||
process.env.DEFAULT_APP_NAME = process.env.DEFAULT_APP_NAME || 'insomnia-app';
|
||||
|
@ -34,6 +34,7 @@ describe.each(compact([npmPackageBinPath, ...binaries]))('inso with %s', binPath
|
||||
srcInsoNedb,
|
||||
['--env', 'Dev'],
|
||||
'Echo Test Suite',
|
||||
'--verbose',
|
||||
);
|
||||
|
||||
expect(failed).toBe(false);
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user