diff --git a/lib/proxy.js b/lib/proxy.js index 44062a0..b7fea5a 100644 --- a/lib/proxy.js +++ b/lib/proxy.js @@ -5,7 +5,7 @@ const http = require('http'), async = require('async'), color = require('colorful'), certMgr = require('./certMgr').default, - Recorder = require('./recorder'), + Recorder = require('./recorder').default, logUtil = require('./log'), util = require('./util').default, events = require('events'), diff --git a/lib/recorder.js b/lib/recorder.ts similarity index 55% rename from lib/recorder.js rename to lib/recorder.ts index f684a49..4488ee6 100644 --- a/lib/recorder.js +++ b/lib/recorder.ts @@ -1,46 +1,75 @@ -'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({ title: 'ws message stringify', type: 'object', properties: { time: { - type: 'integer' + type: 'integer', }, message: { - type: 'string' + type: 'string', }, isToServer: { - type: 'boolean' - } - } + type: 'boolean', + }, + }, }); const BODY_FILE_PRFIX = 'res_body_'; const WS_MESSAGE_FILE_PRFIX = 'ws_message_'; const CACHE_DIR_PREFIX = 'cache_r'; -function getCacheDir() { - const rand = Math.floor(Math.random() * 1000000), - cachePath = path.join(proxyUtil.getAnyProxyPath('cache'), './' + CACHE_DIR_PREFIX + rand); +function getCacheDir(): string { + const rand = Math.floor(Math.random() * 1000000); + const cachePath = path.join(proxyUtil.getAnyProxyPath('cache'), './' + CACHE_DIR_PREFIX + rand); fs.mkdirSync(cachePath); return cachePath; } -function normalizeInfo(id, info) { - const singleRecord = {}; +function normalizeInfo(id: number, info: AnyProxyRecorder.ResourceInfo): ISingleRecord { + const singleRecord: ISingleRecord = {}; - //general + // general singleRecord._id = id; singleRecord.id = id; singleRecord.url = info.url; @@ -48,13 +77,13 @@ function normalizeInfo(id, info) { singleRecord.path = info.path; singleRecord.method = info.method; - //req + // req singleRecord.reqHeader = info.req.headers; singleRecord.startTime = info.startTime; singleRecord.reqBody = info.reqBody || ''; singleRecord.protocol = info.protocol || ''; - //res + // res if (info.endTime) { singleRecord.statusCode = info.statusCode; singleRecord.endTime = info.endTime; @@ -71,7 +100,7 @@ function normalizeInfo(id, info) { } else { singleRecord.statusCode = ''; singleRecord.endTime = ''; - singleRecord.resHeader = ''; + singleRecord.resHeader = {}; singleRecord.length = ''; singleRecord.mime = ''; singleRecord.duration = ''; @@ -81,17 +110,20 @@ function normalizeInfo(id, info) { } class Recorder extends events.EventEmitter { - constructor(config) { - super(config); + private globalId: number; + private cachePath: string; + private db: Datastore; + constructor() { + super(); this.globalId = 1; this.cachePath = getCacheDir(); this.db = new Datastore(); 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; if (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); } - updateRecord(id, info) { - if (id < 0) return; + public updateRecord(id: number, info: AnyProxyRecorder.ResourceInfo): void { + if (id < 0) { + return; + } const self = this; const db = self.db; @@ -125,37 +162,43 @@ class Recorder extends events.EventEmitter { * 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; - if (id < 0) return; + if (id < 0) { + return; + } try { 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) { console.error(e); logUtil.error(e.message + e.stack); } this.emitUpdateLatestWsMessage(id, { - id: id, - message: message + id, + message, }); } - updateExtInfo(id, extInfo) { - const self = this; - const db = self.db; + // public updateExtInfo(id: number , extInfo: any): void { + // const self = this; + // const db = self.db; - db.update({ _id: id }, { $set: { ext: extInfo } }, {}, (err, nums) => { - if (!err) { - self.emitUpdate(id); - } - }); - } + // db.update({ _id: id }, { $set: { ext: extInfo } }, {}, (err, nums) => { + // if (!err) { + // self.emitUpdate(id); + // } + // }); + // } - appendRecord(info) { - if (info.req.headers.anyproxy_web_req) { //TODO request from web interface + public appendRecord(info: AnyProxyRecorder.ResourceInfo): number { + if (info.req.headers.anyproxy_web_req) { // TODO request from web interface return -1; } const self = this; @@ -170,33 +213,43 @@ class Recorder extends events.EventEmitter { return thisId; } - updateRecordBody(id, info) { + public updateRecordBody(id: number, info: AnyProxyRecorder.ResourceInfo): void { const self = this; const cachePath = self.cachePath; - if (id === -1) return; + if (id === -1) { + return; + } - if (!id || typeof info.resBody === 'undefined') return; - //add to body map - //ignore image data + if (!id || typeof info.resBody === 'undefined') { + return; + } + // add to body map + // ignore image data 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 * */ - getBody(id, cb) { + public getBody(id: number, cb: (err: Error, content?: Buffer | string) => void): void { const self = this; const cachePath = self.cachePath; if (id < 0) { - cb && cb(''); + cb && cb(null, ''); } 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) { cb && cb(err); } else { @@ -205,16 +258,25 @@ 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 result = { method: '', type: 'unknown', mime: '', - content: '' + content: '', + fileName: undefined, + statusCode: undefined, }; self.getSingleRecord(id, (err, doc) => { - //check whether this record exists + // check whether this record exists if (!doc || !doc[0]) { cb(new Error('failed to find record for this id')); return; @@ -229,26 +291,27 @@ class Recorder extends events.EventEmitter { } else if (!bodyContent) { cb(null, result); } else { - const record = doc[0], - resHeader = record.resHeader || {}; + const record = doc[0]; + const resHeader = record.resHeader || {}; try { - const headerStr = JSON.stringify(resHeader), - charsetMatch = headerStr.match(/charset='?([a-zA-Z0-9-]+)'?/), - contentType = resHeader && (resHeader['content-type'] || resHeader['Content-Type']); + const headerStr = JSON.stringify(resHeader); + const charsetMatch = headerStr.match(/charset='?([a-zA-Z0-9-]+)'?/); + const contentType = resHeader && (resHeader['content-type'] || resHeader['Content-Type']); if (charsetMatch && charsetMatch.length) { const currentCharset = charsetMatch[1].toLowerCase(); 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.content = bodyContent.toString(); result.type = contentType && /application\/json/i.test(contentType) ? 'json' : 'text'; } else if (contentType && /image/i.test(contentType)) { result.type = 'image'; result.mime = contentType; - result.content = bodyContent; + result.content = (bodyContent as string); } else { result.type = contentType; result.mime = contentType; @@ -269,16 +332,16 @@ class Recorder extends events.EventEmitter { * get decoded WebSoket messages * */ - getDecodedWsMessage(id, cb) { + public getDecodedWsMessage(id: number, cb: (err: Error, messages?: AnyProxyRecorder.WsResourceInfo[]) => void): void { const self = this; const cachePath = self.cachePath; if (id < 0) { - cb && cb([]); + cb && cb(null, []); } 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) { cb && cb(err); } 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 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 db = self.db; 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 db = self.db; limit = limit || 10; idStart = typeof idStart === 'number' ? idStart : (self.globalId - limit); - db.find({ _id: { $gte: parseInt(idStart, 10) } }) + db.find({ _id: { $gte: idStart } }) .sort({ _id: 1 }) .limit(limit) .exec(cb); } - clear() { + public clear(): void { const self = this; proxyUtil.deleteFolderContentsRecursive(self.cachePath, true); } } -module.exports = Recorder; -module.exports.default = Recorder; +export default Recorder; diff --git a/lib/webInterface.js b/lib/webInterface.js index 80fd15c..ff04155 100644 --- a/lib/webInterface.js +++ b/lib/webInterface.js @@ -92,7 +92,7 @@ class webInterface extends events.EventEmitter { app.get('/downloadBody', (req, res) => { const query = req.query; - recorder.getDecodedBody(query.id, (err, result) => { + recorder.getDecodedBody(parseInt(query.id, 10), (err, result) => { if (err || !result || !result.content) { res.json({}); } else if (result.mime) { @@ -116,7 +116,7 @@ class webInterface extends events.EventEmitter { res.setHeader('Access-Control-Allow-Origin', '*'); const query = req.query; if (query && query.id) { - recorder.getDecodedBody(query.id, (err, result) => { + recorder.getDecodedBody(parseInt(query.id, 10), (err, result) => { // 返回下载信息 const _resDownload = function (isDownload) { isDownload = typeof isDownload === 'boolean' ? isDownload : true; @@ -169,7 +169,7 @@ class webInterface extends events.EventEmitter { app.get('/fetchReqBody', (req, res) => { const query = req.query; if (query && query.id) { - recorder.getSingleRecord(query.id, (err, doc) => { + recorder.getSingleRecord(parseInt(query.id, 10), (err, doc) => { if (err || !doc[0]) { console.error(err); res.end(''); @@ -188,7 +188,7 @@ class webInterface extends events.EventEmitter { app.get('/fetchWsMessages', (req, res) => { const query = req.query; if (query && query.id) { - recorder.getDecodedWsMessage(query.id, (err, messages) => { + recorder.getDecodedWsMessage(parseInt(query.id, 10), (err, messages) => { if (err) { console.error(err); res.json([]); diff --git a/typings/index.d.ts b/typings/index.d.ts index abbcd08..63b80b8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -38,11 +38,14 @@ declare namespace AnyProxyRecorder { statusCode?: number, resHeader?: ResponseHeader, host?: string, + protocol?: string, method?: string, path?: string, url?: string, startTime?: number, endTime?: number, + req?: any, + reqBody?: string, res?: { statusCode: number, headers: ResponseHeader