anyproxy/lib/recorder.ts

399 lines
11 KiB
TypeScript
Raw Normal View History

2018-08-28 20:36:44 +08:00
'use strict';
import * as Datastore from 'nedb';
import * as path from 'path';
import * as fs from 'fs';
import * as events from 'events';
import * as iconv from 'iconv-lite';
import * as fastJson from 'fast-json-stringify';
import logUtil from './log';
import proxyUtil from './util';
// //start recording and share a list when required
// const Datastore = require('nedb'),
// path = require('path'),
// fs = require('fs'),
// logUtil = require('./log'),
// events = require('events'),
// iconv = require('iconv-lite'),
// fastJson = require('fast-json-stringify'),
// proxyUtil = require('./util').default;
declare interface ISingleRecord {
_id?: number;
id?: number;
url?: string;
host?: string;
path?: string;
method?: string;
reqHeader?: OneLevelObjectType;
startTime?: number;
reqBody?: string;
protocol?: string;
statusCode?: number | string;
endTime?: number | string;
resHeader?: OneLevelObjectType;
length?: number | string;
mime?: string;
duration?: number | string;
}
2017-12-01 21:30:49 +08:00
const wsMessageStingify = fastJson({
title: 'ws message stringify',
type: 'object',
properties: {
time: {
2018-08-28 20:36:44 +08:00
type: 'integer',
},
message: {
2018-08-28 20:36:44 +08:00
type: 'string',
},
isToServer: {
2018-08-28 20:36:44 +08:00
type: 'boolean',
},
},
});
2017-12-01 21:30:49 +08:00
const BODY_FILE_PRFIX = 'res_body_';
2017-12-12 20:07:06 +08:00
const WS_MESSAGE_FILE_PRFIX = 'ws_message_';
2017-12-01 21:30:49 +08:00
const CACHE_DIR_PREFIX = 'cache_r';
2018-08-28 20:36:44 +08:00
function getCacheDir(): string {
const rand = Math.floor(Math.random() * 1000000);
const cachePath = path.join(proxyUtil.getAnyProxyPath('cache'), './' + CACHE_DIR_PREFIX + rand);
2017-12-01 21:30:49 +08:00
fs.mkdirSync(cachePath);
return cachePath;
}
2014-10-23 11:07:02 +08:00
2018-08-28 20:36:44 +08:00
function normalizeInfo(id: number, info: AnyProxyRecorder.ResourceInfo): ISingleRecord {
const singleRecord: ISingleRecord = {};
2017-12-01 21:30:49 +08:00
2018-08-28 20:36:44 +08:00
// general
2017-12-01 21:30:49 +08:00
singleRecord._id = id;
singleRecord.id = id;
singleRecord.url = info.url;
singleRecord.host = info.host;
singleRecord.path = info.path;
singleRecord.method = info.method;
2018-08-28 20:36:44 +08:00
// req
2017-12-01 21:30:49 +08:00
singleRecord.reqHeader = info.req.headers;
singleRecord.startTime = info.startTime;
singleRecord.reqBody = info.reqBody || '';
singleRecord.protocol = info.protocol || '';
2018-08-28 20:36:44 +08:00
// res
2017-12-01 21:30:49 +08:00
if (info.endTime) {
singleRecord.statusCode = info.statusCode;
singleRecord.endTime = info.endTime;
singleRecord.resHeader = info.resHeader;
singleRecord.length = info.length;
const contentType = info.resHeader['content-type'] || info.resHeader['Content-Type'];
if (contentType) {
singleRecord.mime = contentType.split(';')[0];
} else {
singleRecord.mime = '';
2014-10-23 11:07:02 +08:00
}
2017-12-01 21:30:49 +08:00
singleRecord.duration = info.endTime - info.startTime;
} else {
singleRecord.statusCode = '';
singleRecord.endTime = '';
2018-08-28 20:36:44 +08:00
singleRecord.resHeader = {};
2017-12-01 21:30:49 +08:00
singleRecord.length = '';
singleRecord.mime = '';
singleRecord.duration = '';
}
return singleRecord;
}
class Recorder extends events.EventEmitter {
2018-08-28 20:36:44 +08:00
private globalId: number;
private cachePath: string;
private db: Datastore;
constructor() {
super();
2017-12-01 21:30:49 +08:00
this.globalId = 1;
this.cachePath = getCacheDir();
this.db = new Datastore();
this.db.persistence.setAutocompactionInterval(5001);
2018-08-28 20:36:44 +08:00
// this.recordBodyMap = []; // id - body
2017-12-01 21:30:49 +08:00
}
2018-08-28 20:36:44 +08:00
public emitUpdate(id: number, info?: ISingleRecord): void {
2017-12-01 21:30:49 +08:00
const self = this;
if (info) {
self.emit('update', info);
} else {
self.getSingleRecord(id, (err, doc) => {
if (!err && !!doc && !!doc[0]) {
self.emit('update', doc[0]);
2015-07-08 17:55:14 +08:00
}
2017-12-01 21:30:49 +08:00
});
}
}
2015-07-08 17:55:14 +08:00
2018-08-28 20:36:44 +08:00
public emitUpdateLatestWsMessage(id: number, message: {
id: number,
message: AnyProxyRecorder.WsResourceInfo,
}): void {
2017-12-12 20:07:06 +08:00
this.emit('updateLatestWsMsg', message);
}
2018-08-28 20:36:44 +08:00
public updateRecord(id: number, info: AnyProxyRecorder.ResourceInfo): void {
if (id < 0) {
return;
}
2017-12-01 21:30:49 +08:00
const self = this;
const db = self.db;
2014-08-27 17:42:42 +08:00
2017-12-01 21:30:49 +08:00
const finalInfo = normalizeInfo(id, info);
2014-08-27 17:42:42 +08:00
2017-12-01 21:30:49 +08:00
db.update({ _id: id }, finalInfo);
self.updateRecordBody(id, info);
2014-08-27 17:42:42 +08:00
2017-12-01 21:30:49 +08:00
self.emitUpdate(id, finalInfo);
}
2014-08-27 17:42:42 +08:00
2017-12-12 20:07:06 +08:00
/**
* This method shall be called at each time there are new message
*
*/
2018-08-28 20:36:44 +08:00
public updateRecordWsMessage(id: number, message: AnyProxyRecorder.WsResourceInfo): void {
2017-12-12 20:07:06 +08:00
const cachePath = this.cachePath;
2018-08-28 20:36:44 +08:00
if (id < 0) {
return;
}
2017-12-12 20:07:06 +08:00
try {
const recordWsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
2018-08-28 20:36:44 +08:00
fs.appendFile(recordWsMessageFile, wsMessageStingify(message) + ',', (err) => {
if (err) {
logUtil.error(err.message);
}
});
2017-12-12 20:07:06 +08:00
} catch (e) {
console.error(e);
logUtil.error(e.message + e.stack);
}
this.emitUpdateLatestWsMessage(id, {
2018-08-28 20:36:44 +08:00
id,
message,
2017-12-12 20:07:06 +08:00
});
}
2018-08-28 20:36:44 +08:00
// public updateExtInfo(id: number , extInfo: any): void {
// const self = this;
// const db = self.db;
2015-07-08 17:55:14 +08:00
2018-08-28 20:36:44 +08:00
// db.update({ _id: id }, { $set: { ext: extInfo } }, {}, (err, nums) => {
// if (!err) {
// self.emitUpdate(id);
// }
// });
// }
2017-12-01 21:30:49 +08:00
2018-08-28 20:36:44 +08:00
public appendRecord(info: AnyProxyRecorder.ResourceInfo): number {
if (info.req.headers.anyproxy_web_req) { // TODO request from web interface
2017-12-01 21:30:49 +08:00
return -1;
2015-07-08 17:55:14 +08:00
}
2017-12-01 21:30:49 +08:00
const self = this;
const db = self.db;
2015-07-08 17:55:14 +08:00
2017-12-01 21:30:49 +08:00
const thisId = self.globalId++;
const finalInfo = normalizeInfo(thisId, info);
db.insert(finalInfo);
self.updateRecordBody(thisId, info);
2014-08-27 17:42:42 +08:00
2017-12-01 21:30:49 +08:00
self.emitUpdate(thisId, finalInfo);
return thisId;
}
2014-08-27 17:42:42 +08:00
2018-08-28 20:36:44 +08:00
public updateRecordBody(id: number, info: AnyProxyRecorder.ResourceInfo): void {
2017-12-01 21:30:49 +08:00
const self = this;
const cachePath = self.cachePath;
2014-08-27 17:42:42 +08:00
2018-08-28 20:36:44 +08:00
if (id === -1) {
return;
}
2018-08-28 20:36:44 +08:00
if (!id || typeof info.resBody === 'undefined') {
return;
}
// add to body map
// ignore image data
2017-12-01 21:30:49 +08:00
const bodyFile = path.join(cachePath, BODY_FILE_PRFIX + id);
2018-08-28 20:36:44 +08:00
fs.writeFile(bodyFile, info.resBody, (err) => {
if (err) {
logUtil.error(err.name);
}
});
2017-12-01 21:30:49 +08:00
}
2014-08-27 17:42:42 +08:00
2017-12-12 20:07:06 +08:00
/**
* get body and websocket file
*
*/
2018-08-28 20:36:44 +08:00
public getBody(id: number, cb: (err: Error, content?: Buffer | string) => void): void {
2017-12-01 21:30:49 +08:00
const self = this;
const cachePath = self.cachePath;
2014-08-27 17:42:42 +08:00
2017-12-01 21:30:49 +08:00
if (id < 0) {
2018-08-28 20:36:44 +08:00
cb && cb(null, '');
2017-12-01 21:30:49 +08:00
}
2014-08-27 17:42:42 +08:00
2017-12-01 21:30:49 +08:00
const bodyFile = path.join(cachePath, BODY_FILE_PRFIX + id);
2018-08-28 20:36:44 +08:00
// node exported the `constants` from fs to maintain all the state constans since V7
// but the property `constants` does not exists in versions below 7, so we keep the way
fs.access(bodyFile, (fs as any).F_OK || (fs as any).R_OK, (err) => {
2017-12-01 21:30:49 +08:00
if (err) {
cb && cb(err);
} else {
fs.readFile(bodyFile, cb);
}
});
}
2018-08-28 20:36:44 +08:00
public getDecodedBody(id: number, cb: (err: Error, result?: {
method?: string;
type?: string;
mime?: string;
content?: string;
fileName?: string;
statusCode?: number;
}) => void): void {
2017-12-01 21:30:49 +08:00
const self = this;
const result = {
2017-12-12 20:07:06 +08:00
method: '',
2017-12-01 21:30:49 +08:00
type: 'unknown',
mime: '',
2018-08-28 20:36:44 +08:00
content: '',
fileName: undefined,
statusCode: undefined,
2014-08-27 17:42:42 +08:00
};
2017-12-01 21:30:49 +08:00
self.getSingleRecord(id, (err, doc) => {
2018-08-28 20:36:44 +08:00
// check whether this record exists
2017-12-01 21:30:49 +08:00
if (!doc || !doc[0]) {
cb(new Error('failed to find record for this id'));
return;
}
2017-12-12 20:07:06 +08:00
// also put the `method` back, so the client can decide whether to load ws messages
result.method = doc[0].method;
2017-12-01 21:30:49 +08:00
self.getBody(id, (error, bodyContent) => {
if (error) {
cb(error);
} else if (!bodyContent) {
cb(null, result);
} else {
2018-08-28 20:36:44 +08:00
const record = doc[0];
const resHeader = record.resHeader || {};
2017-12-01 21:30:49 +08:00
try {
2018-08-28 20:36:44 +08:00
const headerStr = JSON.stringify(resHeader);
const charsetMatch = headerStr.match(/charset='?([a-zA-Z0-9-]+)'?/);
const contentType = resHeader && (resHeader['content-type'] || resHeader['Content-Type']);
2017-12-01 21:30:49 +08:00
if (charsetMatch && charsetMatch.length) {
const currentCharset = charsetMatch[1].toLowerCase();
if (currentCharset !== 'utf-8' && iconv.encodingExists(currentCharset)) {
2018-08-28 20:36:44 +08:00
result.content = iconv.decode((bodyContent as Buffer), currentCharset);
} else {
result.content = bodyContent.toString();
2017-12-01 21:30:49 +08:00
}
result.mime = contentType;
result.type = contentType && /application\/json/i.test(contentType) ? 'json' : 'text';
} else if (contentType && /image/i.test(contentType)) {
result.type = 'image';
result.mime = contentType;
2018-08-28 20:36:44 +08:00
result.content = (bodyContent as string);
2017-12-01 21:30:49 +08:00
} else {
result.type = contentType;
result.mime = contentType;
result.content = bodyContent.toString();
2015-04-20 09:39:27 +08:00
}
2017-12-01 21:30:49 +08:00
result.fileName = path.basename(record.path);
result.statusCode = record.statusCode;
} catch (e) {
console.error(e);
}
cb(null, result);
2014-08-27 17:42:42 +08:00
}
2017-12-01 21:30:49 +08:00
});
});
}
2017-12-12 20:07:06 +08:00
/**
* get decoded WebSoket messages
*
*/
2018-08-28 20:36:44 +08:00
public getDecodedWsMessage(id: number, cb: (err: Error, messages?: AnyProxyRecorder.WsResourceInfo[]) => void): void {
2017-12-12 20:07:06 +08:00
const self = this;
const cachePath = self.cachePath;
if (id < 0) {
2018-08-28 20:36:44 +08:00
cb && cb(null, []);
2017-12-12 20:07:06 +08:00
}
const wsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
2018-08-28 20:36:44 +08:00
fs.access(wsMessageFile, (fs as any).F_OK || (fs as any).R_OK, (err) => {
2017-12-12 20:07:06 +08:00
if (err) {
cb && cb(err);
} else {
fs.readFile(wsMessageFile, 'utf8', (error, content) => {
if (error) {
cb && cb(err);
}
try {
// remove the last dash "," if it has, since it's redundant
// and also add brackets to make it a complete JSON structure
content = `[${content.replace(/,$/, '')}]`;
const messages = JSON.parse(content);
cb(null, messages);
} catch (e) {
console.error(e);
logUtil.error(e.message + e.stack);
cb(e);
}
});
}
});
}
2018-08-28 20:36:44 +08:00
public getSingleRecord(id: number, cb: (err: Error, result: ISingleRecord) => void): void {
2017-12-01 21:30:49 +08:00
const self = this;
const db = self.db;
2018-08-28 20:36:44 +08:00
db.find({ _id: id }, cb);
2017-12-01 21:30:49 +08:00
}
2018-08-28 20:36:44 +08:00
public getSummaryList(cb: (err: Error, records: ISingleRecord[]) => void): void {
2017-12-01 21:30:49 +08:00
const self = this;
const db = self.db;
db.find({}, cb);
}
2018-08-28 20:36:44 +08:00
public getRecords(idStart: number | string, limit: number, cb: (err: Error, records: ISingleRecord[]) => void): void {
2017-12-01 21:30:49 +08:00
const self = this;
const db = self.db;
limit = limit || 10;
idStart = typeof idStart === 'number' ? idStart : (self.globalId - limit);
2018-08-28 20:36:44 +08:00
db.find({ _id: { $gte: idStart } })
2017-12-01 21:30:49 +08:00
.sort({ _id: 1 })
.limit(limit)
.exec(cb);
}
2018-08-28 20:36:44 +08:00
public clear(): void {
2017-12-01 21:30:49 +08:00
const self = this;
proxyUtil.deleteFolderContentsRecursive(self.cachePath, true);
}
2014-08-27 17:42:42 +08:00
}
2018-08-28 20:36:44 +08:00
export default Recorder;