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-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' ;
2021-05-12 06:35:00 +00:00
import { database as db , Query } from '../common/database' ;
import { getDataDirectory } from '../common/electron-helpers' ;
2016-09-21 20:32:45 +00:00
2016-11-22 19:42:10 +00:00
export const name = 'Response' ;
2021-05-12 06:35:00 +00:00
2016-10-02 20:57:00 +00:00
export const type = 'Response' ;
2021-05-12 06:35:00 +00:00
2016-10-02 20:57:00 +00:00
export const prefix = 'res' ;
2021-05-12 06:35:00 +00:00
2017-03-23 22:10:42 +00:00
export const canDuplicate = false ;
2016-11-22 19:42:10 +00:00
2021-05-12 06:35:00 +00:00
export const canSync = false ;
2017-07-18 22:10:57 +00:00
2021-05-12 06:35:00 +00:00
export interface ResponseHeader {
name : string ;
value : string ;
}
2017-07-18 22:10:57 +00:00
2021-05-12 06:35:00 +00:00
export interface ResponseTimelineEntry {
name : string ;
timestamp : number ;
value : string ;
}
2017-07-18 22:10:57 +00:00
2021-05-12 06:35:00 +00:00
type Compression = 'zip' | null | '__NEEDS_MIGRATION__' | undefined ;
interface BaseResponse {
environmentId : string | null ;
statusCode : number ;
statusMessage : string ;
httpVersion : string ;
contentType : string ;
url : string ;
bytesRead : number ;
bytesContent : number ;
elapsedTime : number ;
2021-05-18 20:32:18 +00:00
headers : ResponseHeader [ ] ;
2021-05-12 06:35:00 +00:00
bodyPath : string ;
// Actual bodies are stored on the filesystem
timelinePath : string ;
// Actual timelines are stored on the filesystem
bodyCompression : Compression ;
error : string ;
requestVersionId : string | null ;
2017-07-18 22:10:57 +00:00
// Things from the request
2021-05-12 06:35:00 +00:00
settingStoreCookies : boolean | null ;
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 ,
2021-05-12 06:35:00 +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 : [ ] ,
2021-05-12 06:35:00 +00:00
timelinePath : '' ,
// Actual timelines are stored on the filesystem
bodyPath : '' ,
// Actual bodies are stored on the filesystem
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
2021-05-12 06:35:00 +00:00
export async function migrate ( doc : Response ) {
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 ;
}
2021-05-12 06:35:00 +00:00
export function hookDatabaseInit ( consoleLog : typeof console . log = console . log ) {
2021-02-25 21:10:21 +00:00
consoleLog ( '[db] 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
}
2021-05-12 06:35:00 +00:00
export function hookRemove ( doc : Response , consoleLog : typeof console . log = console . log ) {
2019-04-27 08:46:10 +00:00
fs . unlink ( doc . bodyPath , ( ) = > {
2020-07-06 22:59:56 +00:00
consoleLog ( ` [response] Delete body ${ doc . bodyPath } ` ) ;
2019-04-27 08:46:10 +00:00
} ) ;
fs . unlink ( doc . timelinePath , ( ) = > {
2020-07-06 22:59:56 +00:00
consoleLog ( ` [response] Delete timeline ${ doc . timelinePath } ` ) ;
2019-04-27 08:46:10 +00:00
} ) ;
2018-06-18 21:13:56 +00:00
}
2018-06-25 17:42:50 +00:00
export function getById ( id : string ) {
2021-05-12 06:35:00 +00:00
return db . get < Response > ( type , id ) ;
2016-11-27 21:42:38 +00:00
}
2021-05-12 06:35:00 +00:00
export async function all() {
return db . all < Response > ( type ) ;
2016-11-27 21:42:38 +00:00
}
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 ( ) ;
2021-05-12 06:35:00 +00:00
const query : Record < string , any > = {
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 ,
2021-05-12 06:35:00 +00:00
) {
const query : Query = {
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 ;
}
2021-05-12 06:35:00 +00:00
return db . findMostRecentlyModified < Response > ( 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 ,
2021-05-12 06:35:00 +00:00
) {
2020-01-22 19:23:19 +00:00
const responses = await _findRecentForRequest ( requestId , environmentId , 1 ) ;
2021-05-12 06:35:00 +00:00
const response = responses [ 0 ] as Response | null | undefined ;
2017-11-01 11:23:22 +00:00
return response || null ;
2017-02-20 18:32:27 +00:00
}
2021-05-12 06:35:00 +00:00
export async function create ( patch : Record < string , any > = { } , maxResponses = 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 ;
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
2021-05-12 06:35:00 +00:00
const query : Record < string , any > = {
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
2021-05-12 06:35:00 +00:00
const allResponses = await db . findMostRecentlyModified < Response > ( 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
2021-05-12 06:35:00 +00:00
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 ) {
2021-05-12 06:35:00 +00:00
return db . getMostRecentlyModified < Response > ( type , {
parentId ,
} ) ;
2016-10-02 20:57:00 +00:00
}
2017-06-30 03:30:22 +00:00
2021-05-12 06:35:00 +00:00
export function getBodyStream < T extends Response , TFail extends Readable > (
response : T ,
readFailureValue? : TFail | null ,
) {
2018-10-17 16:42:33 +00:00
return getBodyStreamFromPath ( response . bodyPath || '' , response . bodyCompression , readFailureValue ) ;
2017-11-21 17:49:17 +00:00
}
2021-05-12 06:35:00 +00:00
export const getBodyBuffer = < TFail = null > (
response ? : { bodyPath? : string , bodyCompression? : Compression } ,
readFailureValue? : TFail | null ,
) = > getBodyBufferFromPath (
response ? . bodyPath || '' ,
response ? . bodyCompression || null ,
readFailureValue ,
) ;
2017-11-23 21:57:08 +00:00
2021-05-12 06:35:00 +00:00
export function getTimeline ( response : Response ) {
2019-04-27 08:46:10 +00:00
return getTimelineFromPath ( response . timelinePath || '' ) ;
}
2021-05-12 06:35:00 +00:00
function getBodyStreamFromPath < TFail extends Readable > (
2017-11-23 21:57:08 +00:00
bodyPath : string ,
2021-05-12 06:35:00 +00:00
compression : Compression ,
readFailureValue? : TFail | null ,
) : Readable | null | TFail {
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 ) ;
2021-05-12 06:35:00 +00:00
2017-11-23 21:57:08 +00:00
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 ,
2021-05-12 06:35:00 +00:00
compression : Compression ,
readFailureValue? : T | null ,
) {
2017-11-23 21:57:08 +00:00
// 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 ) ;
2021-05-12 06:35:00 +00:00
2017-11-23 21:57:08 +00:00
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
}
}
2021-05-12 06:35:00 +00:00
function getTimelineFromPath ( timelinePath : string ) {
2019-04-27 08:46:10 +00:00
// No body, so return empty Buffer
if ( ! timelinePath ) {
return [ ] ;
}
try {
const rawBuffer = fs . readFileSync ( timelinePath ) ;
2021-05-18 20:32:18 +00:00
return JSON . parse ( rawBuffer . toString ( ) ) as ResponseTimelineEntry [ ] ;
2019-04-27 08:46:10 +00:00
} catch ( err ) {
console . warn ( 'Failed to read response body' , err . message ) ;
return [ ] ;
}
}
2021-05-12 06:35:00 +00:00
async function migrateBodyToFileSystem ( doc : Response ) {
2017-06-30 03:30:22 +00:00
if ( doc . hasOwnProperty ( 'body' ) && doc . _id && ! doc . bodyPath ) {
2021-05-12 06:35:00 +00:00
// @ts-expect-error -- TSCONVERSION previously doc.body and doc.encoding did exist but are now removed, and if they exist we want to migrate away from them
2017-06-30 03:30:22 +00:00
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 ) ;
}
2021-05-12 06:35:00 +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
2021-05-12 06:35:00 +00:00
function migrateBodyCompression ( doc : Response ) {
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
2021-05-12 06:35:00 +00:00
async function migrateTimelineToFileSystem ( doc : Response ) {
2019-04-27 08:46:10 +00:00
if ( doc . hasOwnProperty ( 'timeline' ) && doc . _id && ! doc . timelinePath ) {
const dir = path . join ( getDataDirectory ( ) , 'responses' ) ;
mkdirp . sync ( dir ) ;
2021-05-12 06:35:00 +00:00
// @ts-expect-error -- TSCONVERSION previously doc.timeline did exist but is now removed, and if it exists we want to migrate away from it
2019-04-27 08:46:10 +00:00
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 ) ;
}
2021-05-12 06:35:00 +00:00
return db . docUpdate ( doc , {
timelinePath : fsPath ,
} ) ;
2019-04-27 08:46:10 +00:00
} 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 ) ;
2021-05-12 06:35:00 +00:00
2018-12-05 03:26:18 +00:00
if ( files . length === 0 ) {
return ;
}
2021-05-18 20:32:18 +00:00
const whitelistFiles : string [ ] = [ ] ;
2021-05-12 06:35:00 +00:00
for ( const r of ( await db . all < Response > ( type ) || [ ] ) ) {
2019-04-27 08:46:10 +00:00
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
}
}