2017-07-18 22:10:57 +00:00
|
|
|
// @flow
|
2018-06-25 17:42:50 +00:00
|
|
|
import type { BaseModel } from './index';
|
2017-07-19 04:48:28 +00:00
|
|
|
import * as models from './index';
|
2018-06-25 17:42:50 +00:00
|
|
|
import { Readable } from 'stream';
|
2017-07-19 04:48:28 +00:00
|
|
|
|
2017-06-30 03:30:22 +00:00
|
|
|
import fs from 'fs';
|
|
|
|
import crypto from 'crypto';
|
|
|
|
import path from 'path';
|
2017-11-23 21:57:08 +00:00
|
|
|
import zlib from 'zlib';
|
2017-06-30 03:30:22 +00:00
|
|
|
import mkdirp from 'mkdirp';
|
|
|
|
import * as db from '../common/database';
|
2018-11-06 02:39:50 +00:00
|
|
|
import { getDataDirectory } from '../common/misc';
|
2016-09-21 20:32:45 +00:00
|
|
|
|
2016-11-22 19:42:10 +00:00
|
|
|
export const name = 'Response';
|
2016-10-02 20:57:00 +00:00
|
|
|
export const type = 'Response';
|
|
|
|
export const prefix = 'res';
|
2017-03-23 22:10:42 +00:00
|
|
|
export const canDuplicate = false;
|
2019-04-18 00:50:03 +00:00
|
|
|
export const canSync = false;
|
2016-11-22 19:42:10 +00:00
|
|
|
|
2017-07-18 22:10:57 +00:00
|
|
|
export type ResponseHeader = {
|
|
|
|
name: string,
|
2018-12-12 17:36:11 +00:00
|
|
|
value: string,
|
2018-06-25 17:42:50 +00:00
|
|
|
};
|
2017-07-18 22:10:57 +00:00
|
|
|
|
|
|
|
export type ResponseTimelineEntry = {
|
|
|
|
name: string,
|
2018-12-12 17:36:11 +00:00
|
|
|
value: string,
|
2018-06-25 17:42:50 +00:00
|
|
|
};
|
2017-07-18 22:10:57 +00:00
|
|
|
|
2017-07-19 04:48:28 +00:00
|
|
|
type BaseResponse = {
|
2020-01-22 19:23:19 +00:00
|
|
|
environmentId: string | null,
|
2017-07-18 22:10:57 +00:00
|
|
|
statusCode: number,
|
|
|
|
statusMessage: string,
|
2017-11-13 23:10:53 +00:00
|
|
|
httpVersion: string,
|
2017-07-18 22:10:57 +00:00
|
|
|
contentType: string,
|
|
|
|
url: string,
|
|
|
|
bytesRead: number,
|
2017-08-11 21:44:34 +00:00
|
|
|
bytesContent: number,
|
2017-07-18 22:10:57 +00:00
|
|
|
elapsedTime: number,
|
|
|
|
headers: Array<ResponseHeader>,
|
|
|
|
bodyPath: string, // Actual bodies are stored on the filesystem
|
2019-04-27 08:46:10 +00:00
|
|
|
timelinePath: string, // Actual timelines are stored on the filesystem
|
2017-11-28 11:25:05 +00:00
|
|
|
bodyCompression: 'zip' | null | '__NEEDS_MIGRATION__',
|
2017-07-18 22:10:57 +00:00
|
|
|
error: string,
|
|
|
|
requestVersionId: string | null,
|
|
|
|
|
|
|
|
// Things from the request
|
|
|
|
settingStoreCookies: boolean | null,
|
2018-12-12 17:36:11 +00:00
|
|
|
settingSendCookies: boolean | null,
|
2017-07-18 22:10:57 +00:00
|
|
|
};
|
|
|
|
|
2017-07-19 04:48:28 +00:00
|
|
|
export type Response = BaseModel & BaseResponse;
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function init(): BaseResponse {
|
2016-11-10 01:15:27 +00:00
|
|
|
return {
|
2016-10-02 20:57:00 +00:00
|
|
|
statusCode: 0,
|
|
|
|
statusMessage: '',
|
2017-11-13 23:10:53 +00:00
|
|
|
httpVersion: '',
|
2016-10-27 16:45:44 +00:00
|
|
|
contentType: '',
|
2016-10-02 20:57:00 +00:00
|
|
|
url: '',
|
|
|
|
bytesRead: 0,
|
2017-08-11 21:44:34 +00:00
|
|
|
bytesContent: -1, // -1 means that it was legacy and this property didn't exist yet
|
2016-10-02 20:57:00 +00:00
|
|
|
elapsedTime: 0,
|
|
|
|
headers: [],
|
2019-04-27 08:46:10 +00:00
|
|
|
timelinePath: '', // Actual timelines are stored on the filesystem
|
2017-06-30 03:30:22 +00:00
|
|
|
bodyPath: '', // Actual bodies are stored on the filesystem
|
2017-11-28 11:25:05 +00:00
|
|
|
bodyCompression: '__NEEDS_MIGRATION__', // For legacy bodies
|
2017-03-29 23:09:28 +00:00
|
|
|
error: '',
|
2017-06-12 21:49:46 +00:00
|
|
|
requestVersionId: null,
|
2017-03-29 23:09:28 +00:00
|
|
|
|
|
|
|
// Things from the request
|
|
|
|
settingStoreCookies: null,
|
2018-12-12 17:36:11 +00:00
|
|
|
settingSendCookies: null,
|
2020-02-11 18:45:47 +00:00
|
|
|
|
|
|
|
// Responses sent before environment filtering will have a special value
|
|
|
|
// so they don't show up at all when filtering is on.
|
|
|
|
environmentId: '__LEGACY__',
|
2017-03-03 20:09:08 +00:00
|
|
|
};
|
2016-10-02 20:57:00 +00:00
|
|
|
}
|
2016-09-21 20:32:45 +00:00
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export async function migrate(doc: Object) {
|
2017-11-28 11:25:05 +00:00
|
|
|
doc = await migrateBodyToFileSystem(doc);
|
|
|
|
doc = await migrateBodyCompression(doc);
|
2019-04-27 08:46:10 +00:00
|
|
|
doc = await migrateTimelineToFileSystem(doc);
|
2016-11-22 19:42:10 +00:00
|
|
|
return doc;
|
|
|
|
}
|
|
|
|
|
2020-01-22 19:23:19 +00:00
|
|
|
export function hookDatabaseInit() {
|
2018-12-03 18:43:43 +00:00
|
|
|
console.log('Init responses DB');
|
2020-01-22 19:23:19 +00:00
|
|
|
process.nextTick(async () => {
|
|
|
|
await models.response.cleanDeletedResponses();
|
|
|
|
});
|
2018-12-03 18:43:43 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function hookRemove(doc: Response) {
|
2019-04-27 08:46:10 +00:00
|
|
|
fs.unlink(doc.bodyPath, () => {
|
|
|
|
console.log(`[response] Delete body ${doc.bodyPath}`);
|
|
|
|
});
|
|
|
|
fs.unlink(doc.timelinePath, () => {
|
|
|
|
console.log(`[response] Delete timeline ${doc.timelinePath}`);
|
|
|
|
});
|
2018-06-18 21:13:56 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function getById(id: string) {
|
2016-11-27 21:42:38 +00:00
|
|
|
return db.get(type, id);
|
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
export async function all(): Promise<Array<Response>> {
|
2016-11-27 21:42:38 +00:00
|
|
|
return db.all(type);
|
|
|
|
}
|
|
|
|
|
2020-01-22 19:23:19 +00:00
|
|
|
export async function removeForRequest(parentId: string, environmentId?: string | null) {
|
2020-02-11 18:45:47 +00:00
|
|
|
const settings = await models.settings.getOrCreate();
|
|
|
|
const query: Object = {
|
2020-01-22 19:23:19 +00:00
|
|
|
parentId,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Only add if not undefined. null is not the same as undefined
|
|
|
|
// null: find responses sent from base environment
|
|
|
|
// undefined: find all responses
|
2020-02-11 18:45:47 +00:00
|
|
|
if (environmentId !== undefined && settings.filterResponsesByEnv) {
|
|
|
|
query.environmentId = environmentId;
|
2020-01-22 19:23:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Also delete legacy responses here or else the user will be confused as to
|
|
|
|
// why some responses are still showing in the UI.
|
2020-02-11 18:45:47 +00:00
|
|
|
await db.removeWhere(type, query);
|
2016-11-27 21:42:38 +00:00
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function remove(response: Response) {
|
2017-07-19 04:48:28 +00:00
|
|
|
return db.remove(response);
|
2017-06-12 21:48:17 +00:00
|
|
|
}
|
|
|
|
|
2020-01-22 19:23:19 +00:00
|
|
|
async function _findRecentForRequest(
|
2017-11-21 17:49:17 +00:00
|
|
|
requestId: string,
|
2020-01-22 19:23:19 +00:00
|
|
|
environmentId: string | null,
|
2018-12-12 17:36:11 +00:00
|
|
|
limit: number,
|
2017-11-21 17:49:17 +00:00
|
|
|
): Promise<Array<Response>> {
|
2020-02-11 19:24:52 +00:00
|
|
|
const query: Object = {
|
2020-01-22 19:23:19 +00:00
|
|
|
parentId: requestId,
|
|
|
|
};
|
|
|
|
|
2020-02-11 18:45:47 +00:00
|
|
|
// Filter responses by environment if setting is enabled
|
|
|
|
if ((await models.settings.getOrCreate()).filterResponsesByEnv) {
|
|
|
|
query.environmentId = environmentId;
|
|
|
|
}
|
|
|
|
|
|
|
|
return db.findMostRecentlyModified(type, query, limit);
|
2016-11-27 21:42:38 +00:00
|
|
|
}
|
|
|
|
|
2020-01-22 19:23:19 +00:00
|
|
|
export async function getLatestForRequest(
|
|
|
|
requestId: string,
|
|
|
|
environmentId: string | null,
|
|
|
|
): Promise<Response | null> {
|
|
|
|
const responses = await _findRecentForRequest(requestId, environmentId, 1);
|
2017-11-01 11:23:22 +00:00
|
|
|
const response = (responses[0]: ?Response);
|
|
|
|
return response || null;
|
2017-02-20 18:32:27 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 00:50:03 +00:00
|
|
|
export async function create(patch: Object = {}, maxResponses: number = 20) {
|
2016-09-21 20:32:45 +00:00
|
|
|
if (!patch.parentId) {
|
|
|
|
throw new Error('New Response missing `parentId`');
|
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
const { parentId } = patch;
|
2016-11-27 21:42:38 +00:00
|
|
|
|
2017-06-12 21:49:46 +00:00
|
|
|
// Create request version snapshot
|
|
|
|
const request = await models.request.getById(parentId);
|
2018-10-17 16:42:33 +00:00
|
|
|
const requestVersion = request ? await models.requestVersion.create(request) : null;
|
2017-06-12 21:49:46 +00:00
|
|
|
patch.requestVersionId = requestVersion ? requestVersion._id : null;
|
|
|
|
|
2020-02-11 18:45:47 +00:00
|
|
|
// Filter responses by environment if setting is enabled
|
2020-02-11 19:24:52 +00:00
|
|
|
const query: Object = { parentId };
|
2020-02-11 18:45:47 +00:00
|
|
|
if (
|
|
|
|
(await models.settings.getOrCreate()).filterResponsesByEnv &&
|
|
|
|
patch.hasOwnProperty('environmentId')
|
|
|
|
) {
|
|
|
|
query.environmentId = patch.environmentId;
|
|
|
|
}
|
|
|
|
|
2016-11-27 21:42:38 +00:00
|
|
|
// Delete all other responses before creating the new one
|
2020-02-11 18:45:47 +00:00
|
|
|
const allResponses = await db.findMostRecentlyModified(type, query, Math.max(1, maxResponses));
|
2016-11-27 21:42:38 +00:00
|
|
|
const recentIds = allResponses.map(r => r._id);
|
2020-02-11 18:45:47 +00:00
|
|
|
|
|
|
|
// Remove all that were in the last query, except the first `maxResponses` IDs
|
|
|
|
await db.removeWhere(type, { ...query, _id: { $nin: recentIds } });
|
2016-11-27 21:42:38 +00:00
|
|
|
|
|
|
|
// Actually create the new response
|
2017-11-21 17:49:17 +00:00
|
|
|
return db.docCreate(type, patch);
|
2016-10-02 20:57:00 +00:00
|
|
|
}
|
2016-09-21 20:32:45 +00:00
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
export function getLatestByParentId(parentId: string) {
|
|
|
|
return db.getMostRecentlyModified(type, { parentId });
|
2016-10-02 20:57:00 +00:00
|
|
|
}
|
2017-06-30 03:30:22 +00:00
|
|
|
|
2018-10-17 16:42:33 +00:00
|
|
|
export function getBodyStream<T>(response: Object, readFailureValue: ?T): Readable | null | T {
|
|
|
|
return getBodyStreamFromPath(response.bodyPath || '', response.bodyCompression, readFailureValue);
|
2017-11-21 17:49:17 +00:00
|
|
|
}
|
|
|
|
|
2018-10-17 16:42:33 +00:00
|
|
|
export function getBodyBuffer<T>(response: Object, readFailureValue: ?T): Buffer | T | null {
|
|
|
|
return getBodyBufferFromPath(response.bodyPath || '', response.bodyCompression, readFailureValue);
|
2017-11-23 21:57:08 +00:00
|
|
|
}
|
|
|
|
|
2019-04-27 08:46:10 +00:00
|
|
|
export function getTimeline(response: Object): Array<ResponseTimelineEntry> {
|
|
|
|
return getTimelineFromPath(response.timelinePath || '');
|
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
function getBodyStreamFromPath<T>(
|
2017-11-23 21:57:08 +00:00
|
|
|
bodyPath: string,
|
|
|
|
compression: string | null,
|
2018-12-12 17:36:11 +00:00
|
|
|
readFailureValue: ?T,
|
2017-11-23 21:57:08 +00:00
|
|
|
): Readable | null | T {
|
2017-06-30 03:30:22 +00:00
|
|
|
// No body, so return empty Buffer
|
2017-11-23 21:57:08 +00:00
|
|
|
if (!bodyPath) {
|
|
|
|
return null;
|
2017-06-30 03:30:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2017-11-23 21:57:08 +00:00
|
|
|
fs.statSync(bodyPath);
|
2017-06-30 03:30:22 +00:00
|
|
|
} catch (err) {
|
|
|
|
console.warn('Failed to read response body', err.message);
|
2017-11-23 21:57:08 +00:00
|
|
|
return readFailureValue === undefined ? null : readFailureValue;
|
2017-06-30 03:30:22 +00:00
|
|
|
}
|
|
|
|
|
2017-11-23 21:57:08 +00:00
|
|
|
const readStream = fs.createReadStream(bodyPath);
|
|
|
|
if (compression === 'zip') {
|
|
|
|
return readStream.pipe(zlib.createGunzip());
|
|
|
|
} else {
|
|
|
|
return readStream;
|
|
|
|
}
|
|
|
|
}
|
2017-06-30 03:30:22 +00:00
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
function getBodyBufferFromPath<T>(
|
2017-11-23 21:57:08 +00:00
|
|
|
bodyPath: string,
|
|
|
|
compression: string | null,
|
2018-12-12 17:36:11 +00:00
|
|
|
readFailureValue: ?T,
|
2017-11-23 21:57:08 +00:00
|
|
|
): Buffer | T | null {
|
|
|
|
// No body, so return empty Buffer
|
|
|
|
if (!bodyPath) {
|
|
|
|
return Buffer.alloc(0);
|
|
|
|
}
|
2017-06-30 03:30:22 +00:00
|
|
|
|
|
|
|
try {
|
2017-11-23 21:57:08 +00:00
|
|
|
const rawBuffer = fs.readFileSync(bodyPath);
|
|
|
|
if (compression === 'zip') {
|
|
|
|
return zlib.gunzipSync(rawBuffer);
|
|
|
|
} else {
|
|
|
|
return rawBuffer;
|
|
|
|
}
|
2017-06-30 03:30:22 +00:00
|
|
|
} catch (err) {
|
2017-11-23 21:57:08 +00:00
|
|
|
console.warn('Failed to read response body', err.message);
|
|
|
|
return readFailureValue === undefined ? null : readFailureValue;
|
2017-06-30 03:30:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-27 08:46:10 +00:00
|
|
|
function getTimelineFromPath(timelinePath: string): Array<ResponseTimelineEntry> {
|
|
|
|
// No body, so return empty Buffer
|
|
|
|
if (!timelinePath) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const rawBuffer = fs.readFileSync(timelinePath);
|
|
|
|
return JSON.parse(rawBuffer.toString());
|
|
|
|
} catch (err) {
|
|
|
|
console.warn('Failed to read response body', err.message);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
async function migrateBodyToFileSystem(doc: Object) {
|
2017-06-30 03:30:22 +00:00
|
|
|
if (doc.hasOwnProperty('body') && doc._id && !doc.bodyPath) {
|
|
|
|
const bodyBuffer = Buffer.from(doc.body, doc.encoding || 'utf8');
|
2019-04-27 08:46:10 +00:00
|
|
|
const dir = path.join(getDataDirectory(), 'responses');
|
2017-11-23 21:57:08 +00:00
|
|
|
|
|
|
|
mkdirp.sync(dir);
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
const hash = crypto
|
|
|
|
.createHash('md5')
|
|
|
|
.update(bodyBuffer || '')
|
|
|
|
.digest('hex');
|
2017-11-23 21:57:08 +00:00
|
|
|
const bodyPath = path.join(dir, `${hash}.zip`);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const buff = bodyBuffer || Buffer.from('');
|
|
|
|
fs.writeFileSync(bodyPath, buff);
|
|
|
|
} catch (err) {
|
|
|
|
console.warn('Failed to write response body to file', err.message);
|
|
|
|
}
|
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
return db.docUpdate(doc, { bodyPath, bodyCompression: null });
|
2017-06-30 03:30:22 +00:00
|
|
|
} else {
|
|
|
|
return doc;
|
|
|
|
}
|
|
|
|
}
|
2017-11-23 21:57:08 +00:00
|
|
|
|
2018-06-25 17:42:50 +00:00
|
|
|
function migrateBodyCompression(doc: Object) {
|
2017-11-28 11:25:05 +00:00
|
|
|
if (doc.bodyCompression === '__NEEDS_MIGRATION__') {
|
|
|
|
doc.bodyCompression = 'zip';
|
2017-11-23 21:57:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return doc;
|
|
|
|
}
|
2018-12-05 03:26:18 +00:00
|
|
|
|
2019-04-27 08:46:10 +00:00
|
|
|
async function migrateTimelineToFileSystem(doc: Object) {
|
|
|
|
if (doc.hasOwnProperty('timeline') && doc._id && !doc.timelinePath) {
|
|
|
|
const dir = path.join(getDataDirectory(), 'responses');
|
|
|
|
|
|
|
|
mkdirp.sync(dir);
|
|
|
|
const timelineStr = JSON.stringify(doc.timeline, null, '\t');
|
|
|
|
const fsPath = doc.bodyPath + '.timeline';
|
|
|
|
|
|
|
|
try {
|
|
|
|
fs.writeFileSync(fsPath, timelineStr);
|
|
|
|
} catch (err) {
|
|
|
|
console.warn('Failed to write response body to file', err.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
return db.docUpdate(doc, { timelinePath: fsPath });
|
|
|
|
} else {
|
|
|
|
return doc;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-05 03:26:18 +00:00
|
|
|
export async function cleanDeletedResponses() {
|
|
|
|
const responsesDir = path.join(getDataDirectory(), 'responses');
|
|
|
|
mkdirp.sync(responsesDir);
|
|
|
|
|
2019-04-27 08:46:10 +00:00
|
|
|
const files = fs.readdirSync(responsesDir);
|
2018-12-05 03:26:18 +00:00
|
|
|
if (files.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-04-27 08:46:10 +00:00
|
|
|
const whitelistFiles = [];
|
|
|
|
for (const r of await db.all(type)) {
|
|
|
|
whitelistFiles.push(r.bodyPath.slice(responsesDir.length + 1));
|
|
|
|
whitelistFiles.push(r.timelinePath.slice(responsesDir.length + 1));
|
|
|
|
}
|
2018-12-05 03:26:18 +00:00
|
|
|
|
2019-04-27 08:46:10 +00:00
|
|
|
for (const filePath of files) {
|
|
|
|
if (whitelistFiles.indexOf(filePath) >= 0) {
|
|
|
|
continue;
|
2018-12-05 03:26:18 +00:00
|
|
|
}
|
2019-04-27 08:46:10 +00:00
|
|
|
|
2020-01-22 19:23:19 +00:00
|
|
|
try {
|
|
|
|
fs.unlinkSync(path.join(responsesDir, filePath));
|
|
|
|
} catch (err) {
|
|
|
|
// Just keep going, doesn't matter
|
|
|
|
}
|
2018-12-05 03:26:18 +00:00
|
|
|
}
|
|
|
|
}
|