feat: refact recorder.js into TS

This commit is contained in:
砚然 2018-08-28 20:36:44 +08:00
parent 5bf6a55323
commit 4497160284
4 changed files with 148 additions and 83 deletions

View File

@ -5,7 +5,7 @@ const http = require('http'),
async = require('async'), async = require('async'),
color = require('colorful'), color = require('colorful'),
certMgr = require('./certMgr').default, certMgr = require('./certMgr').default,
Recorder = require('./recorder'), Recorder = require('./recorder').default,
logUtil = require('./log'), logUtil = require('./log'),
util = require('./util').default, util = require('./util').default,
events = require('events'), events = require('events'),

View File

@ -1,44 +1,73 @@
'use strict' '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;
}
//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;
const wsMessageStingify = fastJson({ const wsMessageStingify = fastJson({
title: 'ws message stringify', title: 'ws message stringify',
type: 'object', type: 'object',
properties: { properties: {
time: { time: {
type: 'integer' type: 'integer',
}, },
message: { message: {
type: 'string' type: 'string',
}, },
isToServer: { isToServer: {
type: 'boolean' type: 'boolean',
} },
} },
}); });
const BODY_FILE_PRFIX = 'res_body_'; const BODY_FILE_PRFIX = 'res_body_';
const WS_MESSAGE_FILE_PRFIX = 'ws_message_'; const WS_MESSAGE_FILE_PRFIX = 'ws_message_';
const CACHE_DIR_PREFIX = 'cache_r'; const CACHE_DIR_PREFIX = 'cache_r';
function getCacheDir() { function getCacheDir(): string {
const rand = Math.floor(Math.random() * 1000000), const rand = Math.floor(Math.random() * 1000000);
cachePath = path.join(proxyUtil.getAnyProxyPath('cache'), './' + CACHE_DIR_PREFIX + rand); const cachePath = path.join(proxyUtil.getAnyProxyPath('cache'), './' + CACHE_DIR_PREFIX + rand);
fs.mkdirSync(cachePath); fs.mkdirSync(cachePath);
return cachePath; return cachePath;
} }
function normalizeInfo(id, info) { function normalizeInfo(id: number, info: AnyProxyRecorder.ResourceInfo): ISingleRecord {
const singleRecord = {}; const singleRecord: ISingleRecord = {};
// general // general
singleRecord._id = id; singleRecord._id = id;
@ -71,7 +100,7 @@ function normalizeInfo(id, info) {
} else { } else {
singleRecord.statusCode = ''; singleRecord.statusCode = '';
singleRecord.endTime = ''; singleRecord.endTime = '';
singleRecord.resHeader = ''; singleRecord.resHeader = {};
singleRecord.length = ''; singleRecord.length = '';
singleRecord.mime = ''; singleRecord.mime = '';
singleRecord.duration = ''; singleRecord.duration = '';
@ -81,17 +110,20 @@ function normalizeInfo(id, info) {
} }
class Recorder extends events.EventEmitter { class Recorder extends events.EventEmitter {
constructor(config) { private globalId: number;
super(config); private cachePath: string;
private db: Datastore;
constructor() {
super();
this.globalId = 1; this.globalId = 1;
this.cachePath = getCacheDir(); this.cachePath = getCacheDir();
this.db = new Datastore(); this.db = new Datastore();
this.db.persistence.setAutocompactionInterval(5001); this.db.persistence.setAutocompactionInterval(5001);
this.recordBodyMap = []; // id - body // this.recordBodyMap = []; // id - body
} }
emitUpdate(id, info) { public emitUpdate(id: number, info?: ISingleRecord): void {
const self = this; const self = this;
if (info) { if (info) {
self.emit('update', info); self.emit('update', info);
@ -104,12 +136,17 @@ class Recorder extends events.EventEmitter {
} }
} }
emitUpdateLatestWsMessage(id, message) { public emitUpdateLatestWsMessage(id: number, message: {
id: number,
message: AnyProxyRecorder.WsResourceInfo,
}): void {
this.emit('updateLatestWsMsg', message); this.emit('updateLatestWsMsg', message);
} }
updateRecord(id, info) { public updateRecord(id: number, info: AnyProxyRecorder.ResourceInfo): void {
if (id < 0) return; if (id < 0) {
return;
}
const self = this; const self = this;
const db = self.db; const db = self.db;
@ -125,36 +162,42 @@ class Recorder extends events.EventEmitter {
* This method shall be called at each time there are new message * This method shall be called at each time there are new message
* *
*/ */
updateRecordWsMessage(id, message) { public updateRecordWsMessage(id: number, message: AnyProxyRecorder.WsResourceInfo): void {
const cachePath = this.cachePath; const cachePath = this.cachePath;
if (id < 0) return; if (id < 0) {
return;
}
try { try {
const recordWsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id); const recordWsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
fs.appendFile(recordWsMessageFile, wsMessageStingify(message) + ',', () => {}); fs.appendFile(recordWsMessageFile, wsMessageStingify(message) + ',', (err) => {
if (err) {
logUtil.error(err.message);
}
});
} catch (e) { } catch (e) {
console.error(e); console.error(e);
logUtil.error(e.message + e.stack); logUtil.error(e.message + e.stack);
} }
this.emitUpdateLatestWsMessage(id, { this.emitUpdateLatestWsMessage(id, {
id: id, id,
message: message message,
}); });
} }
updateExtInfo(id, extInfo) { // public updateExtInfo(id: number , extInfo: any): void {
const self = this; // const self = this;
const db = self.db; // const db = self.db;
db.update({ _id: id }, { $set: { ext: extInfo } }, {}, (err, nums) => { // db.update({ _id: id }, { $set: { ext: extInfo } }, {}, (err, nums) => {
if (!err) { // if (!err) {
self.emitUpdate(id); // self.emitUpdate(id);
} // }
}); // });
} // }
appendRecord(info) { public appendRecord(info: AnyProxyRecorder.ResourceInfo): number {
if (info.req.headers.anyproxy_web_req) { // TODO request from web interface if (info.req.headers.anyproxy_web_req) { // TODO request from web interface
return -1; return -1;
} }
@ -170,33 +213,43 @@ class Recorder extends events.EventEmitter {
return thisId; return thisId;
} }
updateRecordBody(id, info) { public updateRecordBody(id: number, info: AnyProxyRecorder.ResourceInfo): void {
const self = this; const self = this;
const cachePath = self.cachePath; const cachePath = self.cachePath;
if (id === -1) return; if (id === -1) {
return;
}
if (!id || typeof info.resBody === 'undefined') return; if (!id || typeof info.resBody === 'undefined') {
return;
}
// add to body map // add to body map
// ignore image data // ignore image data
const bodyFile = path.join(cachePath, BODY_FILE_PRFIX + id); const bodyFile = path.join(cachePath, BODY_FILE_PRFIX + id);
fs.writeFile(bodyFile, info.resBody, () => {}); fs.writeFile(bodyFile, info.resBody, (err) => {
if (err) {
logUtil.error(err.name);
}
});
} }
/** /**
* get body and websocket file * get body and websocket file
* *
*/ */
getBody(id, cb) { public getBody(id: number, cb: (err: Error, content?: Buffer | string) => void): void {
const self = this; const self = this;
const cachePath = self.cachePath; const cachePath = self.cachePath;
if (id < 0) { if (id < 0) {
cb && cb(''); cb && cb(null, '');
} }
const bodyFile = path.join(cachePath, BODY_FILE_PRFIX + id); const bodyFile = path.join(cachePath, BODY_FILE_PRFIX + id);
fs.access(bodyFile, fs.F_OK || fs.R_OK, (err) => { // 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) => {
if (err) { if (err) {
cb && cb(err); cb && cb(err);
} else { } else {
@ -205,13 +258,22 @@ class Recorder extends events.EventEmitter {
}); });
} }
getDecodedBody(id, cb) { public getDecodedBody(id: number, cb: (err: Error, result?: {
method?: string;
type?: string;
mime?: string;
content?: string;
fileName?: string;
statusCode?: number;
}) => void): void {
const self = this; const self = this;
const result = { const result = {
method: '', method: '',
type: 'unknown', type: 'unknown',
mime: '', mime: '',
content: '' content: '',
fileName: undefined,
statusCode: undefined,
}; };
self.getSingleRecord(id, (err, doc) => { self.getSingleRecord(id, (err, doc) => {
// check whether this record exists // check whether this record exists
@ -229,26 +291,27 @@ class Recorder extends events.EventEmitter {
} else if (!bodyContent) { } else if (!bodyContent) {
cb(null, result); cb(null, result);
} else { } else {
const record = doc[0], const record = doc[0];
resHeader = record.resHeader || {}; const resHeader = record.resHeader || {};
try { try {
const headerStr = JSON.stringify(resHeader), const headerStr = JSON.stringify(resHeader);
charsetMatch = headerStr.match(/charset='?([a-zA-Z0-9-]+)'?/), const charsetMatch = headerStr.match(/charset='?([a-zA-Z0-9-]+)'?/);
contentType = resHeader && (resHeader['content-type'] || resHeader['Content-Type']); const contentType = resHeader && (resHeader['content-type'] || resHeader['Content-Type']);
if (charsetMatch && charsetMatch.length) { if (charsetMatch && charsetMatch.length) {
const currentCharset = charsetMatch[1].toLowerCase(); const currentCharset = charsetMatch[1].toLowerCase();
if (currentCharset !== 'utf-8' && iconv.encodingExists(currentCharset)) { if (currentCharset !== 'utf-8' && iconv.encodingExists(currentCharset)) {
bodyContent = iconv.decode(bodyContent, currentCharset); result.content = iconv.decode((bodyContent as Buffer), currentCharset);
} else {
result.content = bodyContent.toString();
} }
result.mime = contentType; result.mime = contentType;
result.content = bodyContent.toString();
result.type = contentType && /application\/json/i.test(contentType) ? 'json' : 'text'; result.type = contentType && /application\/json/i.test(contentType) ? 'json' : 'text';
} else if (contentType && /image/i.test(contentType)) { } else if (contentType && /image/i.test(contentType)) {
result.type = 'image'; result.type = 'image';
result.mime = contentType; result.mime = contentType;
result.content = bodyContent; result.content = (bodyContent as string);
} else { } else {
result.type = contentType; result.type = contentType;
result.mime = contentType; result.mime = contentType;
@ -269,16 +332,16 @@ class Recorder extends events.EventEmitter {
* get decoded WebSoket messages * get decoded WebSoket messages
* *
*/ */
getDecodedWsMessage(id, cb) { public getDecodedWsMessage(id: number, cb: (err: Error, messages?: AnyProxyRecorder.WsResourceInfo[]) => void): void {
const self = this; const self = this;
const cachePath = self.cachePath; const cachePath = self.cachePath;
if (id < 0) { if (id < 0) {
cb && cb([]); cb && cb(null, []);
} }
const wsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id); const wsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
fs.access(wsMessageFile, fs.F_OK || fs.R_OK, (err) => { fs.access(wsMessageFile, (fs as any).F_OK || (fs as any).R_OK, (err) => {
if (err) { if (err) {
cb && cb(err); cb && cb(err);
} else { } else {
@ -303,34 +366,33 @@ class Recorder extends events.EventEmitter {
}); });
} }
getSingleRecord(id, cb) { public getSingleRecord(id: number, cb: (err: Error, result: ISingleRecord) => void): void {
const self = this; const self = this;
const db = self.db; const db = self.db;
db.find({ _id: parseInt(id, 10) }, cb); db.find({ _id: id }, cb);
} }
getSummaryList(cb) { public getSummaryList(cb: (err: Error, records: ISingleRecord[]) => void): void {
const self = this; const self = this;
const db = self.db; const db = self.db;
db.find({}, cb); db.find({}, cb);
} }
getRecords(idStart, limit, cb) { public getRecords(idStart: number | string, limit: number, cb: (err: Error, records: ISingleRecord[]) => void): void {
const self = this; const self = this;
const db = self.db; const db = self.db;
limit = limit || 10; limit = limit || 10;
idStart = typeof idStart === 'number' ? idStart : (self.globalId - limit); idStart = typeof idStart === 'number' ? idStart : (self.globalId - limit);
db.find({ _id: { $gte: parseInt(idStart, 10) } }) db.find({ _id: { $gte: idStart } })
.sort({ _id: 1 }) .sort({ _id: 1 })
.limit(limit) .limit(limit)
.exec(cb); .exec(cb);
} }
clear() { public clear(): void {
const self = this; const self = this;
proxyUtil.deleteFolderContentsRecursive(self.cachePath, true); proxyUtil.deleteFolderContentsRecursive(self.cachePath, true);
} }
} }
module.exports = Recorder; export default Recorder;
module.exports.default = Recorder;

View File

@ -92,7 +92,7 @@ class webInterface extends events.EventEmitter {
app.get('/downloadBody', (req, res) => { app.get('/downloadBody', (req, res) => {
const query = req.query; const query = req.query;
recorder.getDecodedBody(query.id, (err, result) => { recorder.getDecodedBody(parseInt(query.id, 10), (err, result) => {
if (err || !result || !result.content) { if (err || !result || !result.content) {
res.json({}); res.json({});
} else if (result.mime) { } else if (result.mime) {
@ -116,7 +116,7 @@ class webInterface extends events.EventEmitter {
res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Origin', '*');
const query = req.query; const query = req.query;
if (query && query.id) { if (query && query.id) {
recorder.getDecodedBody(query.id, (err, result) => { recorder.getDecodedBody(parseInt(query.id, 10), (err, result) => {
// 返回下载信息 // 返回下载信息
const _resDownload = function (isDownload) { const _resDownload = function (isDownload) {
isDownload = typeof isDownload === 'boolean' ? isDownload : true; isDownload = typeof isDownload === 'boolean' ? isDownload : true;
@ -169,7 +169,7 @@ class webInterface extends events.EventEmitter {
app.get('/fetchReqBody', (req, res) => { app.get('/fetchReqBody', (req, res) => {
const query = req.query; const query = req.query;
if (query && query.id) { if (query && query.id) {
recorder.getSingleRecord(query.id, (err, doc) => { recorder.getSingleRecord(parseInt(query.id, 10), (err, doc) => {
if (err || !doc[0]) { if (err || !doc[0]) {
console.error(err); console.error(err);
res.end(''); res.end('');
@ -188,7 +188,7 @@ class webInterface extends events.EventEmitter {
app.get('/fetchWsMessages', (req, res) => { app.get('/fetchWsMessages', (req, res) => {
const query = req.query; const query = req.query;
if (query && query.id) { if (query && query.id) {
recorder.getDecodedWsMessage(query.id, (err, messages) => { recorder.getDecodedWsMessage(parseInt(query.id, 10), (err, messages) => {
if (err) { if (err) {
console.error(err); console.error(err);
res.json([]); res.json([]);

3
typings/index.d.ts vendored
View File

@ -38,11 +38,14 @@ declare namespace AnyProxyRecorder {
statusCode?: number, statusCode?: number,
resHeader?: ResponseHeader, resHeader?: ResponseHeader,
host?: string, host?: string,
protocol?: string,
method?: string, method?: string,
path?: string, path?: string,
url?: string, url?: string,
startTime?: number, startTime?: number,
endTime?: number, endTime?: number,
req?: any,
reqBody?: string,
res?: { res?: {
statusCode: number, statusCode: number,
headers: ResponseHeader headers: ResponseHeader