diff --git a/bin/anyproxy b/bin/anyproxy index 537cbfb..d373535 100755 --- a/bin/anyproxy +++ b/bin/anyproxy @@ -6,7 +6,7 @@ const program = require('commander'), color = require('colorful'), co = require('co'), packageInfo = require('../package.json'), - util = require('../dist/util'), + util = require('../dist/util').default, rootCACheck = require('./rootCaCheck'), startServer = require('./startServer'), certMgr = require('../dist/certMgr'), diff --git a/lib/certMgr.js b/lib/certMgr.js index 1136ad8..07e73e4 100644 --- a/lib/certMgr.js +++ b/lib/certMgr.js @@ -5,7 +5,7 @@ const co = require('co'); const os = require('os'); const inquirer = require('inquirer'); -const util = require('./util'); +const util = require('./util').default; const logUtil = require('./log'); const options = { diff --git a/lib/httpsServerMgr.js b/lib/httpsServerMgr.js index e66c840..6ce6380 100644 --- a/lib/httpsServerMgr.js +++ b/lib/httpsServerMgr.js @@ -8,7 +8,7 @@ const async = require('async'), color = require('colorful'), certMgr = require('./certMgr'), logUtil = require('./log'), - util = require('./util'), + util = require('./util').default, wsServerMgr = require('./wsServerMgr'), co = require('co'), constants = require('constants'), @@ -194,4 +194,4 @@ class httpsServerMgr { } } -module.exports = httpsServerMgr; +export default httpsServerMgr; diff --git a/lib/log.ts b/lib/log.ts index 1cbf52c..e4a02e4 100644 --- a/lib/log.ts +++ b/lib/log.ts @@ -1,109 +1,109 @@ 'use strict'; - import * as color from 'colorful'; - import util from './util'; +import * as color from 'colorful'; +import util from './util'; - let ifPrint = true; - let logLevel = 0; - enum LogLevelMap { - tip = 0, - system_error = 1, - error = 1, - rule_error = 2, - warn = 3, - debug = 4, - }; +let ifPrint = true; +let logLevel = 0; +enum LogLevelMap { + tip = 0, + system_error = 1, + error = 1, + rule_error = 2, + warn = 3, + debug = 4, +}; - function setPrintStatus(status: boolean): void { - ifPrint = !!status; +function setPrintStatus(status: boolean): void { + ifPrint = !!status; +} + +function setLogLevel(level: string): void { + logLevel = parseInt(level, 10); +} + +function printLog(content: string, type?: LogLevelMap) { + if (!ifPrint) { + return; } - function setLogLevel(level: string): void { - logLevel = parseInt(level, 10); - } - - function printLog(content: string, type?: LogLevelMap) { - if (!ifPrint) { - return; + const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss'); + switch (type) { + case LogLevelMap.tip: { + if (logLevel > 0) { + return; + } + console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content)); + break; } - const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss'); - switch (type) { - case LogLevelMap.tip: { - if (logLevel > 0) { - return; - } - console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content)); - break; + case LogLevelMap.system_error: { + if (logLevel > 1) { + return; } + console.error(color.red(`[AnyProxy ERROR][${timeString}]: ` + content)); + break; + } - case LogLevelMap.system_error: { - if (logLevel > 1) { - return; - } - console.error(color.red(`[AnyProxy ERROR][${timeString}]: ` + content)); - break; - } - - case LogLevelMap.rule_error: { - if (logLevel > 2) { - return; - } - - console.error(color.red(`[AnyProxy RULE_ERROR][${timeString}]: ` + content)); - break; - } - - case LogLevelMap.warn: { - if (logLevel > 3) { - return; - } - - console.error(color.magenta(`[AnyProxy WARN][${timeString}]: ` + content)); - break; - } - - case LogLevelMap.debug: { - console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content)); + case LogLevelMap.rule_error: { + if (logLevel > 2) { return; } - default: { - console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content)); - break; + console.error(color.red(`[AnyProxy RULE_ERROR][${timeString}]: ` + content)); + break; + } + + case LogLevelMap.warn: { + if (logLevel > 3) { + return; } + + console.error(color.magenta(`[AnyProxy WARN][${timeString}]: ` + content)); + break; + } + + case LogLevelMap.debug: { + console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content)); + return; + } + + default: { + console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content)); + break; } } +} - module.exports.printLog = printLog; +module.exports.printLog = printLog; - function debug (content): void { - printLog(content, LogLevelMap.debug); - }; +function debug (content): void { + printLog(content, LogLevelMap.debug); +}; - function info (content): void { - printLog(content, LogLevelMap.tip); - }; +function info (content): void { + printLog(content, LogLevelMap.tip); +}; - function warn (content) { - printLog(content, LogLevelMap.warn); - }; +function warn (content) { + printLog(content, LogLevelMap.warn); +}; - function error (content) { - printLog(content, LogLevelMap.system_error); - }; +function error (content) { + printLog(content, LogLevelMap.system_error); +}; - function ruleError (content) { - printLog(content, LogLevelMap.rule_error); - }; +function ruleError (content) { + printLog(content, LogLevelMap.rule_error); +}; - module.exports.setPrintStatus = setPrintStatus; - module.exports.setLogLevel = setLogLevel; - module.exports.T_TIP = LogLevelMap.tip; - module.exports.T_ERR = LogLevelMap.system_error; - module.exports.T_RULE_ERROR = LogLevelMap.rule_error; - module.exports.T_WARN = LogLevelMap.warn; - module.exports.T_DEBUG = LogLevelMap.debug; +module.exports.setPrintStatus = setPrintStatus; +module.exports.setLogLevel = setLogLevel; +module.exports.T_TIP = LogLevelMap.tip; +module.exports.T_ERR = LogLevelMap.system_error; +module.exports.T_RULE_ERROR = LogLevelMap.rule_error; +module.exports.T_WARN = LogLevelMap.warn; +module.exports.T_DEBUG = LogLevelMap.debug; const LogUtil = { setPrintStatus, @@ -118,8 +118,8 @@ const LogUtil = { T_ERR: LogLevelMap.error, T_RULE_ERROR: LogLevelMap.rule_error, T_WARN: LogLevelMap.warn, - T_DEBUG: LogLevelMap.debug -} + T_DEBUG: LogLevelMap.debug, +}; +exports.LogUtil = LogUtil; export default LogUtil; -module.exports = LogUtil; diff --git a/lib/proxy.js b/lib/proxy.js index cbeebfd..85c25bc 100644 --- a/lib/proxy.js +++ b/lib/proxy.js @@ -7,7 +7,7 @@ const http = require('http'), certMgr = require('./certMgr'), Recorder = require('./recorder'), logUtil = require('./log'), - util = require('./util'), + util = require('./util').default, events = require('events'), co = require('co'), WebInterface = require('./webInterface'), @@ -114,7 +114,7 @@ class ProxyCore extends events.EventEmitter { this.recorder = config.recorder; // init request handler - const RequestHandler = util.freshRequire('./requestHandler'); + const RequestHandler = util.freshRequire('./requestHandler').default; this.requestHandler = new RequestHandler({ wsIntercept: config.wsIntercept, httpServerPort: config.port, // the http server port for http proxy diff --git a/lib/recorder.js b/lib/recorder.js index 1a37739..f684a49 100644 --- a/lib/recorder.js +++ b/lib/recorder.js @@ -8,7 +8,7 @@ const Datastore = require('nedb'), events = require('events'), iconv = require('iconv-lite'), fastJson = require('fast-json-stringify'), - proxyUtil = require('./util'); + proxyUtil = require('./util').default; const wsMessageStingify = fastJson({ title: 'ws message stringify', @@ -333,3 +333,4 @@ class Recorder extends events.EventEmitter { } module.exports = Recorder; +module.exports.default = Recorder; diff --git a/lib/requestHandler/CommonReadableStream.js b/lib/requestHandler/CommonReadableStream.js deleted file mode 100644 index d6ec062..0000000 --- a/lib/requestHandler/CommonReadableStream.js +++ /dev/null @@ -1,16 +0,0 @@ -const Readable = require('stream').Readable; - -const DEFAULT_CHUNK_COLLECT_THRESHOLD = 20 * 1024 * 1024; // about 20 mb - -class CommonReadableStream extends Readable { - constructor(config) { - super({ - highWaterMark: DEFAULT_CHUNK_COLLECT_THRESHOLD * 5 - }); - } - _read(size) { - - } -} - -module.exports = CommonReadableStream; diff --git a/lib/requestHandler/CommonReadableStream.ts b/lib/requestHandler/CommonReadableStream.ts new file mode 100644 index 0000000..8c85901 --- /dev/null +++ b/lib/requestHandler/CommonReadableStream.ts @@ -0,0 +1,17 @@ +import * as stream from 'stream'; +const Readable = stream.Readable; +const DEFAULT_CHUNK_COLLECT_THRESHOLD = 20 * 1024 * 1024; // about 20 mb + +/* tslint:disable:no-empty */ +class CommonReadableStream extends Readable { + constructor() { + super({ + highWaterMark: DEFAULT_CHUNK_COLLECT_THRESHOLD * 5, + }); + } + public _read(): void { + + } +} + +export default CommonReadableStream; diff --git a/lib/requestHandler/UserReqHandler.js b/lib/requestHandler/UserReqHandler.ts similarity index 77% rename from lib/requestHandler/UserReqHandler.js rename to lib/requestHandler/UserReqHandler.ts index cdff75b..d1be4e0 100644 --- a/lib/requestHandler/UserReqHandler.js +++ b/lib/requestHandler/UserReqHandler.ts @@ -1,40 +1,64 @@ /// -import RequestErrorHandler from './requestErrorHandler'; +declare interface ErrorResponse { + statusCode: number; + header: OneLevelObjectType; + body: string; +} -const - url = require('url'), - https = require('https'), - http = require('http'), - color = require('colorful'), - Buffer = require('buffer').Buffer, - util = require('../util'), - Stream = require('stream'), - logUtil = require('../log'), - CommonReadableStream = require('./CommonReadableStream'), - zlib = require('zlib'), - brotliTorb = require('brotli'), - co = require('co'); +import * as url from 'url'; +import * as https from 'https'; +import * as http from 'http'; +import * as color from 'colorful'; +import * as buffer from 'buffer'; +import * as Stream from 'stream'; +import * as zlib from 'zlib'; +import * as brotliTorb from 'brotli'; +import * as co from 'co'; +import util from '../util'; +import logUtil from '../log'; +import RequestErrorHandler from './requestErrorHandler'; +import CommonReadableStream from './CommonReadableStream'; +import Recorder from '../recorder'; +const Buffer = buffer.Buffer; +// const +// url = require('url'), +// https = require('https'), +// http = require('http'), +// color = require('colorful'), +// Buffer = require('buffer').Buffer, +// util = require('../util').default, +// Stream = require('stream'), +// logUtil = require('../log'), +// CommonReadableStream = require('./CommonReadableStream'), +// zlib = require('zlib'), +// brotliTorb = require('brotli'), +// co = require('co'); const requestErrorHandler = new RequestErrorHandler(); const DEFAULT_CHUNK_COLLECT_THRESHOLD = 20 * 1024 * 1024; // about 20 mb // to fix issue with TLS cache, refer to: https://github.com/nodejs/node/issues/8368 -https.globalAgent.maxCachedSessions = 0; +(https.globalAgent as any).maxCachedSessions = 0; /** * fetch remote response * * @param {string} protocol - * @param {object} options options of http.request - * @param {buffer} reqData request body + * @param {object} options + * @param {buffer} reqData * @param {object} config * @param {boolean} config.dangerouslyIgnoreUnauthorized * @param {boolean} config.chunkSizeThreshold * @returns */ -function fetchRemoteResponse(protocol, options, reqData, config) { - reqData = reqData || ''; +function fetchRemoteResponse( + protocol: string, options: https.RequestOptions | http.RequestOptions , + reqData: Buffer, config: { + dangerouslyIgnoreUnauthorized: boolean; + chunkSizeThreshold: number; + }): Promise { + reqData = reqData || Buffer.from(''); return new Promise((resolve, reject) => { delete options.headers['content-length']; // will reset the content-length after rule delete options.headers['Content-Length']; @@ -42,17 +66,18 @@ function fetchRemoteResponse(protocol, options, reqData, config) { delete options.headers['transfer-encoding']; if (config.dangerouslyIgnoreUnauthorized) { - options.rejectUnauthorized = false; + (options as https.RequestOptions).rejectUnauthorized = false; } if (!config.chunkSizeThreshold) { throw new Error('chunkSizeThreshold is required'); } - //send request - const proxyReq = (/https/i.test(protocol) ? https : http).request(options, (res) => { + const finalHttpModule: typeof https | typeof http = /https/i.test(protocol) ? https : http; + // send request + const proxyReq: http.ClientRequest = (finalHttpModule as any).request(options, (res: http.IncomingMessage): void => { res.headers = util.getHeaderFromRawHeaders(res.rawHeaders); - //deal response header + // deal response header const statusCode = res.statusCode; const resHeader = res.headers; let resDataChunks = []; // array of data chunks or stream @@ -69,9 +94,9 @@ function fetchRemoteResponse(protocol, options, reqData, config) { // remove gzip related header, and ungzip the content // note there are other compression types like deflate const contentEncoding = resHeader['content-encoding'] || resHeader['Content-Encoding']; - const ifServerGzipped = /gzip/i.test(contentEncoding); - const isServerDeflated = /deflate/i.test(contentEncoding); - const isBrotlied = /br/i.test(contentEncoding); + const ifServerGzipped = /gzip/i.test((contentEncoding as string)); + const isServerDeflated = /deflate/i.test((contentEncoding as string)); + const isBrotlied = /br/i.test((contentEncoding as string)); /** * when the content is unzipped, update the header content @@ -82,10 +107,10 @@ function fetchRemoteResponse(protocol, options, reqData, config) { delete resHeader['content-encoding']; delete resHeader['Content-Encoding']; } - } + }; // set origin content length into header - resHeader['x-anyproxy-origin-content-length'] = originContentLen; + resHeader['x-anyproxy-origin-content-length'] = '' + originContentLen; // only do unzip when there is res data if (ifServerGzipped && originContentLen) { @@ -133,7 +158,7 @@ function fetchRemoteResponse(protocol, options, reqData, config) { }); }; - //deal response data + // deal response data res.on('data', (chunk) => { rawResChunks.push(chunk); if (resDataStream) { // stream mode @@ -176,16 +201,16 @@ function fetchRemoteResponse(protocol, options, reqData, config) { /* * get error response for exception scenarios */ -function getErrorResponse(error, fullUrl) { +function getErrorResponse(error: NodeJS.ErrnoException, fullUrl: string): ErrorResponse { // default error response const errorResponse = { statusCode: 500, header: { 'Content-Type': 'text/html; charset=utf-8', 'Proxy-Error': true, - 'Proxy-Error-Message': error ? JSON.stringify(error) : 'null' + 'Proxy-Error-Message': error ? JSON.stringify(error) : 'null', }, - body: requestErrorHandler.getErrorContent(error, fullUrl) + body: requestErrorHandler.getErrorContent(error, fullUrl), }; return errorResponse; @@ -193,13 +218,16 @@ function getErrorResponse(error, fullUrl) { export default class UserReqHandler { - constructor(ctx, userRule, recorder) { + public userRule: AnyProxyRule; + public recorder: Recorder; + private reqHandlerCtx: any; + constructor(ctx: any, userRule: AnyProxyRule, recorder: Recorder) { this.userRule = userRule; this.recorder = recorder; this.reqHandlerCtx = ctx; } - handler(req, userRes) { + public handler(req: http.IncomingMessage, userRes: http.ServerResponse): void { /* note req.url is wired @@ -208,7 +236,7 @@ export default class UserReqHandler { */ const self = this; const host = req.headers.host; - const protocol = (!!req.connection.encrypted && !(/^http:/).test(req.url)) ? 'https' : 'http'; + const protocol = (!!(req.connection as any).encrypted && !(/^http:/).test(req.url)) ? 'https' : 'http'; const fullUrl = protocol === 'http' ? req.url : (protocol + '://' + host + req.url); const urlPattern = url.parse(fullUrl); @@ -246,10 +274,10 @@ export default class UserReqHandler { const prepareRequestDetail = () => { const options = { hostname: urlPattern.hostname || req.headers.host, - port: urlPattern.port || req.port || (/https/.test(protocol) ? 443 : 80), + port: urlPattern.port || (req as any).port || (/https/.test(protocol) ? 443 : 80), path, method: req.method, - headers: req.headers + headers: req.headers, }; requestDetail = { @@ -257,7 +285,7 @@ export default class UserReqHandler { protocol, url: fullUrl, requestData: reqData, - _req: req + _req: req, }; return Promise.resolve(); @@ -294,7 +322,7 @@ export default class UserReqHandler { if (!responseInfo) { throw new Error('failed to get response info'); } else if (!responseInfo.statusCode) { - throw new Error('failed to get response status code') + throw new Error('failed to get response status code'); } else if (!responseInfo.header) { throw new Error('filed to get response header'); } @@ -326,7 +354,7 @@ export default class UserReqHandler { } return responseInfo; - } + }; // fetch complete request data co(fetchReqData) @@ -342,29 +370,29 @@ export default class UserReqHandler { protocol, url: protocol + '://' + host + path, req, - startTime: new Date().getTime() + startTime: new Date().getTime(), }; resourceInfoId = self.recorder.appendRecord(resourceInfo); } try { - resourceInfo.reqBody = reqData.toString(); //TODO: deal reqBody in webInterface.js + resourceInfo.reqBody = reqData.toString(); // TODO: deal reqBody in webInterface.js self.recorder && self.recorder.updateRecord(resourceInfoId, resourceInfo); - } catch (e) { } + } catch (e) { console.error(e); } }) // invoke rule before sending request - .then(co.wrap(function *() { + .then(co.wrap(function*(): Generator { const userModifiedInfo = (yield self.userRule.beforeSendRequest(Object.assign({}, requestDetail))) || {}; const finalReqDetail = {}; ['protocol', 'requestOptions', 'requestData', 'response'].map((key) => { - finalReqDetail[key] = userModifiedInfo[key] || requestDetail[key] + finalReqDetail[key] = userModifiedInfo[key] || requestDetail[key]; }); return finalReqDetail; })) // route user config - .then(co.wrap(function *(userConfig) { + .then(co.wrap(function *(userConfig: AnyProxyRequestDetail): Generator { if (userConfig.response) { // user-assigned local response userConfig._directlyPassToRespond = true; @@ -379,7 +407,7 @@ export default class UserReqHandler { statusCode: remoteResponse.statusCode, header: remoteResponse.header, body: remoteResponse.body, - rawBody: remoteResponse.rawBody + rawBody: remoteResponse.rawBody, }, _res: remoteResponse._res, }; @@ -389,18 +417,19 @@ export default class UserReqHandler { })) // invoke rule before responding to client - .then(co.wrap(function *(responseData) { + .then(co.wrap(function*(responseData: AnyProxyReponseDetail): Generator { if (responseData._directlyPassToRespond) { return responseData; } else if (responseData.response.body && responseData.response.body instanceof CommonReadableStream) { // in stream mode return responseData; } else { // TODO: err etimeout - return (yield self.userRule.beforeSendResponse(Object.assign({}, requestDetail), Object.assign({}, responseData))) || responseData; + return (yield self.userRule.beforeSendResponse( + Object.assign({}, requestDetail), Object.assign({}, responseData))) || responseData; } })) - .catch(co.wrap(function *(error) { + .catch(co.wrap(function *(error: NodeJS.ErrnoException): Generator { logUtil.printLog(util.collectErrorLog(error), logUtil.T_ERR); let errorResponse = getErrorResponse(error, fullUrl); @@ -411,18 +440,18 @@ export default class UserReqHandler { if (userResponse && userResponse.response && userResponse.response.header) { errorResponse = userResponse.response; } - } catch (e) { } + } catch (e) { console.error(e); } return { - response: errorResponse + response: errorResponse, }; })) .then(sendFinalResponse) - //update record info + // update record info .then((responseInfo) => { resourceInfo.endTime = new Date().getTime(); - resourceInfo.res = { //construct a self-defined res object + resourceInfo.res = { // construct a self-defined res object statusCode: responseInfo.statusCode, headers: responseInfo.header, }; diff --git a/lib/requestHandler/index.js b/lib/requestHandler/index.ts similarity index 77% rename from lib/requestHandler/index.js rename to lib/requestHandler/index.ts index 76b375c..99e526d 100644 --- a/lib/requestHandler/index.js +++ b/lib/requestHandler/index.ts @@ -1,16 +1,31 @@ 'use strict'; +/// import UserReqHandler from './UserReqHandler'; +import HttpsServerMgr from '../httpsServerMgr'; +import Recorder from '../recorder'; -const - net = require('net'), - color = require('colorful'), - util = require('../util'), - logUtil = require('../log'), - co = require('co'), - WebSocket = require('ws'), - CommonReadableStream = require('./CommonReadableStream'), - HttpsServerMgr = require('../httpsServerMgr'); +import * as color from 'colorful'; +import * as co from 'co'; +import * as net from 'net'; +import * as http from 'http'; +import * as WebSocket from 'ws'; +import util from '../util'; +import logUtil from '../log'; +import CommonReadableStream from './CommonReadableStream'; + +interface IWsReqInfo { + headers: http.IncomingHttpHeaders; + noWsHeaders: http.IncomingHttpHeaders; + hostName: string; + port: string; + path: string; + protocol: 'wss' | 'ws'; +} + +interface IWebsocketOptionHeaders { + [key: string]: string; +} /** * get request info from the ws client, includes: @@ -22,7 +37,7 @@ const @param @required wsClient the ws client of WebSocket * */ -function getWsReqInfo(wsReq) { +function getWsReqInfo(wsReq: http.IncomingMessage): IWsReqInfo { const headers = wsReq.headers || {}; const host = headers.host; const hostName = host.split(':')[0]; @@ -31,12 +46,12 @@ function getWsReqInfo(wsReq) { // TODO 如果是windows机器,url是不是全路径?需要对其过滤,取出 const path = wsReq.url || '/'; - const isEncript = true && wsReq.connection && wsReq.connection.encrypted; + const isEncript = true && wsReq.connection && (wsReq.connection as any).encrypted; /** * construct the request headers based on original connection, * but delete the `sec-websocket-*` headers as they are already consumed by AnyProxy */ - const getNoWsHeaders = () => { + const getNoWsHeaders = (): http.IncomingHttpHeaders => { const originHeaders = Object.assign({}, headers); const originHeaderKeys = Object.keys(originHeaders); originHeaderKeys.forEach((key) => { @@ -49,16 +64,15 @@ function getWsReqInfo(wsReq) { delete originHeaders.connection; delete originHeaders.upgrade; return originHeaders; - } - + }; return { - headers: headers, // the full headers of origin ws connection + headers, // the full headers of origin ws connection noWsHeaders: getNoWsHeaders(), - hostName: hostName, - port: port, - path: path, - protocol: isEncript ? 'wss' : 'ws' + hostName, + port, + path, + protocol: isEncript ? 'wss' : 'ws', }; } /** @@ -70,12 +84,13 @@ function getWsReqInfo(wsReq) { * @param {object} httpsServerMgr * @returns */ -function getConnectReqHandler(userRule, recorder, httpsServerMgr) { - const reqHandlerCtx = this; reqHandlerCtx.conns = new Map(); reqHandlerCtx.cltSockets = new Map() +function getConnectReqHandler(userRule: AnyProxyRule, recorder: Recorder, httpsServerMgr: HttpsServerMgr) + : (req: http.IncomingMessage, socket: net.Socket, head: Buffer[]) => void { + const reqHandlerCtx = this; reqHandlerCtx.conns = new Map(); reqHandlerCtx.cltSockets = new Map(); - return function (req, cltSocket, head) { - const host = req.url.split(':')[0], - targetPort = req.url.split(':')[1]; + return function(req: http.IncomingMessage, cltSocket: net.Socket, head: Buffer[]): void { + const host = req.url.split(':')[0]; + const targetPort = req.url.split(':')[1]; let shouldIntercept; let interceptWsRequest = false; let requestDetail; @@ -90,12 +105,12 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) { 4.1 if (websocket || do_not_intercept) --> pipe to target server 4.2 else --> pipe to local server and do man-in-the-middle attack */ - co(function *() { + co(function *(): Generator { // determine whether to use the man-in-the-middle server logUtil.printLog(color.green('received https CONNECT request ' + host)); requestDetail = { host: req.url, - _req: req + _req: req, }; // the return value in default rule is null // so if the value is null, will take it as final value @@ -106,14 +121,14 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) { shouldIntercept = reqHandlerCtx.forceProxyHttps; } }) - .then(() => - new Promise((resolve) => { + .then(() => { + return new Promise((resolve) => { // mark socket connection as established, to detect the request protocol cltSocket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'UTF-8', resolve); - }) - ) - .then(() => - new Promise((resolve, reject) => { + }); + }) + .then(() => { + return new Promise((resolve, reject) => { let resolved = false; cltSocket.on('data', (chunk) => { requestStream.push(chunk); @@ -139,8 +154,8 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) { cltSocket.on('end', () => { requestStream.push(null); }); - }) - ) + }); + }) .then((result) => { // log and recorder if (shouldIntercept) { @@ -149,7 +164,7 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) { logUtil.printLog('will bypass the man-in-the-middle proxy'); } - //record + // record if (recorder) { resourceInfo = { host, @@ -157,7 +172,7 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) { path: '', url: 'https://' + host, req, - startTime: new Date().getTime() + startTime: new Date().getTime(), }; resourceInfoId = recorder.appendRecord(resourceInfo); } @@ -168,18 +183,19 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) { // server info from the original request const originServer = { host, - port: (targetPort === 80) ? 443 : targetPort - } + port: (targetPort === '80') ? 443 : targetPort, + }; const localHttpServer = { host: 'localhost', - port: reqHandlerCtx.httpServerPort - } + port: reqHandlerCtx.httpServerPort, + }; // for ws request, redirect them to local ws server return interceptWsRequest ? localHttpServer : originServer; } else { - return httpsServerMgr.getSharedHttpsServer(host).then(serverInfo => ({ host: serverInfo.host, port: serverInfo.port })); + return httpsServerMgr.getSharedHttpsServer(host) + .then((serverInfo) => ({ host: serverInfo.host, port: serverInfo.port })); } }) .then((serverInfo) => { @@ -189,7 +205,7 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) { return new Promise((resolve, reject) => { const conn = net.connect(serverInfo.port, serverInfo.host, () => { - //throttle for direct-foward https + // throttle for direct-foward https if (global._throttle && !shouldIntercept) { requestStream.pipe(conn); conn.pipe(global._throttle.throttle()).pipe(cltSocket); @@ -205,8 +221,8 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) { reject(e); }); - reqHandlerCtx.conns.set(serverInfo.host + ':' + serverInfo.port, conn) - reqHandlerCtx.cltSockets.set(serverInfo.host + ':' + serverInfo.port, cltSocket) + reqHandlerCtx.conns.set(serverInfo.host + ':' + serverInfo.port, conn); + reqHandlerCtx.cltSockets.set(serverInfo.host + ':' + serverInfo.port, cltSocket); }); }) .then(() => { @@ -220,40 +236,40 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) { recorder && recorder.updateRecord(resourceInfoId, resourceInfo); } }) - .catch(co.wrap(function *(error) { + .catch(co.wrap(function *(error: Error): Generator { logUtil.printLog(util.collectErrorLog(error), logUtil.T_ERR); try { yield userRule.onConnectError(requestDetail, error); - } catch (e) { } + } catch (e) { console.error(e); } try { let errorHeader = 'Proxy-Error: true\r\n'; errorHeader += 'Proxy-Error-Message: ' + (error || 'null') + '\r\n'; errorHeader += 'Content-Type: text/html\r\n'; cltSocket.write('HTTP/1.1 502\r\n' + errorHeader + '\r\n\r\n'); - } catch (e) { } + } catch (e) { console.error(e); } })); - } + }; } /** * get a websocket event handler - @param @required {object} wsClient +* @param @required {object} wsClient */ -function getWsHandler(userRule, recorder, wsClient, wsReq) { +function getWsHandler(userRule: AnyProxyRule, recorder: Recorder, wsClient: WebSocket, wsReq: http.IncomingMessage): void { const self = this; try { let resourceInfoId = -1; - const resourceInfo = { - wsMessages: [] // all ws messages go through AnyProxy + const resourceInfo: AnyProxyRecorder.ResourceInfo = { + wsMessages: [] as AnyProxyRecorder.WsResourceInfo[], // all ws messages go through AnyProxy }; const clientMsgQueue = []; const serverInfo = getWsReqInfo(wsReq); const wsUrl = `${serverInfo.protocol}://${serverInfo.hostName}:${serverInfo.port}${serverInfo.path}`; const proxyWs = new WebSocket(wsUrl, '', { rejectUnauthorized: !self.dangerouslyIgnoreUnauthorized, - headers: serverInfo.noWsHeaders + headers: serverInfo.noWsHeaders as IWebsocketOptionHeaders, }); if (recorder) { @@ -263,7 +279,7 @@ function getWsHandler(userRule, recorder, wsClient, wsReq) { path: serverInfo.path, url: wsUrl, req: wsReq, - startTime: new Date().getTime() + startTime: new Date().getTime(), }); resourceInfoId = recorder.appendRecord(resourceInfo); } @@ -283,7 +299,7 @@ function getWsHandler(userRule, recorder, wsClient, wsReq) { } else { clientMsgQueue.push(message); } - } + }; /** * consume the message in queue when the proxy ws is not ready yet @@ -294,7 +310,7 @@ function getWsHandler(userRule, recorder, wsClient, wsReq) { const message = clientMsgQueue.shift(); proxyWs.send(message); } - } + }; /** * When the source ws is closed, we need to close the target websocket. @@ -303,7 +319,7 @@ function getWsHandler(userRule, recorder, wsClient, wsReq) { const getCloseFromOriginEvent = (event) => { const code = event.code || ''; const reason = event.reason || ''; - let targetCode = ''; + let targetCode; let targetReason = ''; if (code >= 1004 && code <= 1006) { targetCode = 1000; // normal closure @@ -315,9 +331,9 @@ function getWsHandler(userRule, recorder, wsClient, wsReq) { return { code: targetCode, - reason: targetReason - } - } + reason: targetReason, + }; + }; /** * consruct a message Record from message event @@ -329,7 +345,7 @@ function getWsHandler(userRule, recorder, wsClient, wsReq) { const message = { time: Date.now(), message: messageEvent.data, - isToServer: isToServer + isToServer, }; // resourceInfo.wsMessages.push(message); @@ -338,15 +354,15 @@ function getWsHandler(userRule, recorder, wsClient, wsReq) { proxyWs.onopen = () => { consumeMsgQueue(); - } + }; // this event is fired when the connection is build and headers is returned proxyWs.on('upgrade', (response) => { resourceInfo.endTime = new Date().getTime(); const headers = response.headers; - resourceInfo.res = { //construct a self-defined res object + resourceInfo.res = { // construct a self-defined res object statusCode: response.statusCode, - headers: headers, + headers, }; resourceInfo.statusCode = response.statusCode; @@ -361,29 +377,29 @@ function getWsHandler(userRule, recorder, wsClient, wsReq) { // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes wsClient.close(1001, e.message); proxyWs.close(1001); - } + }; proxyWs.onmessage = (event) => { recordMessage(event, false); wsClient.readyState === 1 && wsClient.send(event.data); - } + }; proxyWs.onclose = (event) => { logUtil.debug(`proxy ws closed with code: ${event.code} and reason: ${event.reason}`); const targetCloseInfo = getCloseFromOriginEvent(event); wsClient.readyState !== 3 && wsClient.close(targetCloseInfo.code, targetCloseInfo.reason); - } + }; wsClient.onmessage = (event) => { recordMessage(event, true); sendProxyMessage(event); - } + }; wsClient.onclose = (event) => { logUtil.debug(`original ws closed with code: ${event.code} and reason: ${event.reason}`); const targetCloseInfo = getCloseFromOriginEvent(event); proxyWs.readyState !== 3 && proxyWs.close(targetCloseInfo.code, targetCloseInfo.reason); - } + }; } catch (e) { logUtil.debug('WebSocket Proxy Error:' + e.message); logUtil.debug(e.stack); @@ -392,7 +408,14 @@ function getWsHandler(userRule, recorder, wsClient, wsReq) { } class RequestHandler { - + public forceProxyHttps: boolean; + public dangerouslyIgnoreUnauthorized: boolean; + public httpServerPort: string; + public wsIntercept: boolean; + public connectReqHandler: () => void; + private userRequestHandler: () => void; + private wsHandler: () => void; + private httpsServerMgr: HttpsServerMgr; /** * Creates an instance of RequestHandler. * @@ -405,7 +428,7 @@ class RequestHandler { * * @memberOf RequestHandler */ - constructor(config, rule, recorder) { + constructor(config: AnyProxyConfig, rule: AnyProxyRule, recorder: Recorder) { const reqHandlerCtx = this; this.forceProxyHttps = false; this.dangerouslyIgnoreUnauthorized = false; @@ -425,8 +448,8 @@ class RequestHandler { } this.httpServerPort = config.httpServerPort; - const default_rule = util.freshRequire('./rule_default'); - const userRule = util.merge(default_rule, rule); + const defaultRule = util.freshRequire('./rule_default'); + const userRule = util.merge(defaultRule, rule); const userReqHandler = new UserReqHandler(reqHandlerCtx, userRule, recorder); reqHandlerCtx.userRequestHandler = userReqHandler.handler.bind(userReqHandler); @@ -434,11 +457,11 @@ class RequestHandler { reqHandlerCtx.httpsServerMgr = new HttpsServerMgr({ handler: reqHandlerCtx.userRequestHandler, - wsHandler: reqHandlerCtx.wsHandler // websocket + wsHandler: reqHandlerCtx.wsHandler, // websocket }); this.connectReqHandler = getConnectReqHandler.apply(reqHandlerCtx, [userRule, recorder, reqHandlerCtx.httpsServerMgr]); } } -module.exports = RequestHandler; +export default RequestHandler; diff --git a/lib/requestHandler/requestErrorHandler.js b/lib/requestHandler/requestErrorHandler.ts similarity index 52% rename from lib/requestHandler/requestErrorHandler.js rename to lib/requestHandler/requestErrorHandler.ts index 8fd9f9e..4f06997 100644 --- a/lib/requestHandler/requestErrorHandler.js +++ b/lib/requestHandler/requestErrorHandler.ts @@ -4,8 +4,8 @@ * handle all request error here, * */ -const pug = require('pug'); -const path = require('path'); +import * as pug from 'pug'; +import * as path from 'path'; const error502PugFn = pug.compileFile(path.join(__dirname, '../resource/502.pug')); const certPugFn = pug.compileFile(path.join(__dirname, '../resource/cert_error.pug')); @@ -13,7 +13,7 @@ const certPugFn = pug.compileFile(path.join(__dirname, '../resource/cert_error.p /** * get error content for certification issues */ -function getCertErrorContent(error, fullUrl) { +function getCertErrorContent(error: NodeJS.ErrnoException, fullUrl: string): string { let content; const title = 'The connection is not private. '; let explain = 'There are error with the certfication of the site.'; @@ -21,21 +21,22 @@ function getCertErrorContent(error, fullUrl) { case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': { explain = 'The certfication of the site you are visiting is not issued by a known agency, ' + 'It usually happenes when the cert is a self-signed one.
' - + 'If you know and trust the site, you can run AnyProxy with option -ignore-unauthorized-ssl to continue.' + + 'If you know and trust the site, you can run AnyProxy with option' + + ' -ignore-unauthorized-ssl to continue.' break; } default: { - explain = '' + explain = ''; break; } } try { content = certPugFn({ - title: title, - explain: explain, - code: error.code + title, + explain, + code: error.code, }); } catch (parseErro) { content = error.stack; @@ -47,14 +48,14 @@ function getCertErrorContent(error, fullUrl) { /* * get the default error content */ -function getDefaultErrorCotent(error, fullUrl) { +function getDefaultErrorCotent(error: NodeJS.ErrnoException, fullUrl: string): string { let content; try { content = error502PugFn({ error, url: fullUrl, - errorStack: error.stack.split(/\n/) + errorStack: error.stack.split(/\n/), }); } catch (parseErro) { content = error.stack; @@ -66,19 +67,22 @@ function getDefaultErrorCotent(error, fullUrl) { /* * get mapped error content for each error */ -module.exports.getErrorContent = function (error, fullUrl) { - let content = ''; - error = error || {}; - switch (error.code) { - case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': { - content = getCertErrorContent(error, fullUrl); - break; - } - default: { - content = getDefaultErrorCotent(error, fullUrl); - break; - } - } +export default class RequestErrorHandler { + public getErrorContent: (error: NodeJS.ErrnoException, fullUrl: string) => string = + function(error: NodeJS.ErrnoException, fullUrl: string): string { + let content = ''; + error = error || { name: 'default_by_anyproxy', message: 'this is the default error'}; + switch (error.code) { + case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': { + content = getCertErrorContent(error, fullUrl); + break; + } + default: { + content = getDefaultErrorCotent(error, fullUrl); + break; + } + } - return content; + return content; + }; } diff --git a/lib/ruleLoader.js b/lib/ruleLoader.js index 1d498ff..8ec9037 100644 --- a/lib/ruleLoader.js +++ b/lib/ruleLoader.js @@ -1,6 +1,6 @@ 'use strict'; -const proxyUtil = require('./util'); +const proxyUtil = require('./util').default; const path = require('path'); const fs = require('fs'); const request = require('request'); diff --git a/lib/util.ts b/lib/util.ts index ce34729..5f15d97 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -17,330 +17,330 @@ import { execSync } from 'child_process'; import logUtil from './log'; - const networkInterfaces = require('os').networkInterfaces(); +const networkInterfaces = require('os').networkInterfaces(); - // {"Content-Encoding":"gzip"} --> {"content-encoding":"gzip"} - function lower_keys (obj: object): object { - for (const key in obj) { - const val = obj[key]; - delete obj[key]; +// {"Content-Encoding":"gzip"} --> {"content-encoding":"gzip"} +function lower_keys (obj: object): object { + for (const key in obj) { + const val = obj[key]; + delete obj[key]; - obj[key.toLowerCase()] = val; - } - - return obj; - }; - - function merge (baseObj: object, extendObj: object): object { - for (const key in extendObj) { - baseObj[key] = extendObj[key]; - } - - return baseObj; - }; - - function getUserHome(): string { - return process.env.HOME || process.env.USERPROFILE; + obj[key.toLowerCase()] = val; } - function getAnyProxyHome(): string { - const home = path.join(getUserHome(), '/.anyproxy/'); - if (!fs.existsSync(home)) { - fs.mkdirSync(home); - } - return home; + return obj; +}; + +function merge (baseObj: object, extendObj: object): object { + for (const key in extendObj) { + baseObj[key] = extendObj[key]; } - function getAnyProxyPath (pathName): string { - const home = getAnyProxyHome(); - const targetPath = path.join(home, pathName); - if (!fs.existsSync(targetPath)) { - fs.mkdirSync(targetPath); - } - return targetPath; + return baseObj; +}; + +function getUserHome(): string { + return process.env.HOME || process.env.USERPROFILE; +} + +function getAnyProxyHome(): string { + const home = path.join(getUserHome(), '/.anyproxy/'); + if (!fs.existsSync(home)) { + fs.mkdirSync(home); } + return home; +} - /** - * 简易字符串render替换 - */ - function simpleRender (str: string, object: object, regexp: RegExp) { - return String(str).replace(regexp || (/\{\{([^{}]+)\}\}/g), (match, name) => { - if (match.charAt(0) === '\\') { - return match.slice(1); - } - return (object[name] != null) ? object[name] : ''; - }); - }; +function getAnyProxyPath (pathName): string { + const home = getAnyProxyHome(); + const targetPath = path.join(home, pathName); + if (!fs.existsSync(targetPath)) { + fs.mkdirSync(targetPath); + } + return targetPath; +} - /** - * 读取指定目录下的子目录 - */ - function filewalker (root: string, cb: Function) { - root = root || process.cwd(); - - const ret = { - directory: [], - file: [] - }; - - fs.readdir(root, (err, list) => { - if (list && list.length) { - list.map((item) => { - const fullPath = path.join(root, item), - stat = fs.lstatSync(fullPath); - - if (stat.isFile()) { - ret.file.push({ - name: item, - fullPath - }); - } else if (stat.isDirectory()) { - ret.directory.push({ - name: item, - fullPath - }); - } - }); - } - - cb && cb.apply(null, [null, ret]); - }); - }; - - /* - * 获取文件所对应的content-type以及content-length等信息 - * 比如在useLocalResponse的时候会使用到 - */ - function contentType (filepath: string): string { - return mime.contentType(path.extname(filepath)) || ''; - }; - - /* - * 读取file的大小,以byte为单位 - */ - function contentLength (filepath: string): number { - try { - const stat = fs.statSync(filepath); - return stat.size; - } catch (e) { - logUtil.printLog(color.red('\nfailed to ready local file : ' + filepath)); - logUtil.printLog(color.red(e)); - return 0; +/** + * 简易字符串render替换 + */ +function simpleRender (str: string, object: object, regexp: RegExp) { + return String(str).replace(regexp || (/\{\{([^{}]+)\}\}/g), (match, name) => { + if (match.charAt(0) === '\\') { + return match.slice(1); } + return (object[name] != null) ? object[name] : ''; + }); +}; + +/** + * 读取指定目录下的子目录 + */ +function filewalker (root: string, cb: Function) { + root = root || process.cwd(); + + const ret = { + directory: [], + file: [] }; - /* - * remove the cache before requiring, the path SHOULD BE RELATIVE TO UTIL.JS - */ - function freshRequire (modulePath: string): NodeModule { - delete require.cache[require.resolve(modulePath)]; - return require(modulePath); - }; + fs.readdir(root, (err, list) => { + if (list && list.length) { + list.map((item) => { + const fullPath = path.join(root, item), + stat = fs.lstatSync(fullPath); - /* - * format the date string - * @param date Date or timestamp - * @param formatter YYYYMMDDHHmmss - */ - function formatDate (date: Date | number, formatter: string): string { - let finalDate : Date; - if (typeof date !== 'object') { - finalDate = new Date(date); + if (stat.isFile()) { + ret.file.push({ + name: item, + fullPath + }); + } else if (stat.isDirectory()) { + ret.directory.push({ + name: item, + fullPath + }); + } + }); + } + + cb && cb.apply(null, [null, ret]); + }); +}; + +/* +* 获取文件所对应的content-type以及content-length等信息 +* 比如在useLocalResponse的时候会使用到 +*/ +function contentType (filepath: string): string { + return mime.contentType(path.extname(filepath)) || ''; +}; + +/* +* 读取file的大小,以byte为单位 +*/ +function contentLength (filepath: string): number { + try { + const stat = fs.statSync(filepath); + return stat.size; + } catch (e) { + logUtil.printLog(color.red('\nfailed to ready local file : ' + filepath)); + logUtil.printLog(color.red(e)); + return 0; + } +}; + +/* +* remove the cache before requiring, the path SHOULD BE RELATIVE TO UTIL.JS +*/ +function freshRequire (modulePath: string): NodeModule { + delete require.cache[require.resolve(modulePath)]; + return require(modulePath); +}; + +/* +* format the date string +* @param date Date or timestamp +* @param formatter YYYYMMDDHHmmss +*/ +function formatDate (date: Date | number, formatter: string): string { + let finalDate : Date; + if (typeof date !== 'object') { + finalDate = new Date(date); + } else { + finalDate = date; + } + const transform = function (value) { + return value < 10 ? '0' + value : value; + }; + return formatter.replace(/^YYYY|MM|DD|hh|mm|ss/g, (match) => { + switch (match) { + case 'YYYY': + return transform(finalDate.getFullYear()); + case 'MM': + return transform(finalDate.getMonth() + 1); + case 'mm': + return transform(finalDate.getMinutes()); + case 'DD': + return transform(finalDate.getDate()); + case 'hh': + return transform(finalDate.getHours()); + case 'ss': + return transform(finalDate.getSeconds()); + default: + return '' + } + }); +}; + + +/** +* get headers(Object) from rawHeaders(Array) +* @param rawHeaders [key, value, key2, value2, ...] + +*/ + +function getHeaderFromRawHeaders (rawHeaders: Array) { + const headerObj = {}; + const _handleSetCookieHeader = function (key, value) { + if (headerObj[key].constructor === Array) { + headerObj[key].push(value); } else { - finalDate = date; + headerObj[key] = [headerObj[key], value]; } - const transform = function (value) { - return value < 10 ? '0' + value : value; - }; - return formatter.replace(/^YYYY|MM|DD|hh|mm|ss/g, (match) => { - switch (match) { - case 'YYYY': - return transform(finalDate.getFullYear()); - case 'MM': - return transform(finalDate.getMonth() + 1); - case 'mm': - return transform(finalDate.getMinutes()); - case 'DD': - return transform(finalDate.getDate()); - case 'hh': - return transform(finalDate.getHours()); - case 'ss': - return transform(finalDate.getSeconds()); - default: - return '' - } - }); }; + if (!!rawHeaders) { + for (let i = 0; i < rawHeaders.length; i += 2) { + const key = rawHeaders[i]; + let value = rawHeaders[i + 1]; - /** - * get headers(Object) from rawHeaders(Array) - * @param rawHeaders [key, value, key2, value2, ...] + if (typeof value === 'string') { + value = value.replace(/\0+$/g, ''); // 去除 \u0000的null字符串 + } - */ - - function getHeaderFromRawHeaders (rawHeaders: Array) { - const headerObj = {}; - const _handleSetCookieHeader = function (key, value) { - if (headerObj[key].constructor === Array) { - headerObj[key].push(value); + if (!headerObj[key]) { + headerObj[key] = value; } else { - headerObj[key] = [headerObj[key], value]; - } - }; - - if (!!rawHeaders) { - for (let i = 0; i < rawHeaders.length; i += 2) { - const key = rawHeaders[i]; - let value = rawHeaders[i + 1]; - - if (typeof value === 'string') { - value = value.replace(/\0+$/g, ''); // 去除 \u0000的null字符串 - } - - if (!headerObj[key]) { - headerObj[key] = value; + // headers with same fields could be combined with comma. Ref: https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + // set-cookie should NOT be combined. Ref: https://tools.ietf.org/html/rfc6265 + if (key.toLowerCase() === 'set-cookie') { + _handleSetCookieHeader(key, value); } else { - // headers with same fields could be combined with comma. Ref: https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 - // set-cookie should NOT be combined. Ref: https://tools.ietf.org/html/rfc6265 - if (key.toLowerCase() === 'set-cookie') { - _handleSetCookieHeader(key, value); - } else { - headerObj[key] = headerObj[key] + ',' + value; - } + headerObj[key] = headerObj[key] + ',' + value; } } } - return headerObj; - }; + } + return headerObj; +}; - function getAllIpAddress (): Array { - const allIp = []; +function getAllIpAddress (): Array { + const allIp = []; - Object.keys(networkInterfaces).map((nic) => { - networkInterfaces[nic].filter((detail) => { - if (detail.family.toLowerCase() === 'ipv4') { - allIp.push(detail.address); - } - }); + Object.keys(networkInterfaces).map((nic) => { + networkInterfaces[nic].filter((detail) => { + if (detail.family.toLowerCase() === 'ipv4') { + allIp.push(detail.address); + } + }); + }); + + return allIp.length ? allIp : ['127.0.0.1']; +}; + +function deleteFolderContentsRecursive(dirPath: string, ifClearFolderItself: boolean): void { + if (!dirPath.trim() || dirPath === '/') { + throw new Error('can_not_delete_this_dir'); + } + + if (fs.existsSync(dirPath)) { + fs.readdirSync(dirPath).forEach((file) => { + const curPath = path.join(dirPath, file); + if (fs.lstatSync(curPath).isDirectory()) { + deleteFolderContentsRecursive(curPath, true); + } else { // delete all files + fs.unlinkSync(curPath); + } }); - return allIp.length ? allIp : ['127.0.0.1']; - }; - - function deleteFolderContentsRecursive(dirPath: string, ifClearFolderItself: boolean): void { - if (!dirPath.trim() || dirPath === '/') { - throw new Error('can_not_delete_this_dir'); - } - - if (fs.existsSync(dirPath)) { - fs.readdirSync(dirPath).forEach((file) => { - const curPath = path.join(dirPath, file); - if (fs.lstatSync(curPath).isDirectory()) { - deleteFolderContentsRecursive(curPath, true); - } else { // delete all files - fs.unlinkSync(curPath); - } - }); - - if (ifClearFolderItself) { - try { - // ref: https://github.com/shelljs/shelljs/issues/49 - const start = Date.now(); - while (true) { - try { - fs.rmdirSync(dirPath); + if (ifClearFolderItself) { + try { + // ref: https://github.com/shelljs/shelljs/issues/49 + const start = Date.now(); + while (true) { + try { + fs.rmdirSync(dirPath); + break; + } catch (er) { + if (process.platform === 'win32' && (er.code === 'ENOTEMPTY' || er.code === 'EBUSY' || er.code === 'EPERM')) { + // Retry on windows, sometimes it takes a little time before all the files in the directory are gone + if (Date.now() - start > 1000) throw er; + } else if (er.code === 'ENOENT') { break; - } catch (er) { - if (process.platform === 'win32' && (er.code === 'ENOTEMPTY' || er.code === 'EBUSY' || er.code === 'EPERM')) { - // Retry on windows, sometimes it takes a little time before all the files in the directory are gone - if (Date.now() - start > 1000) throw er; - } else if (er.code === 'ENOENT') { - break; - } else { - throw er; - } + } else { + throw er; } } - } catch (e) { - throw new Error('could not remove directory (code ' + e.code + '): ' + dirPath); } + } catch (e) { + throw new Error('could not remove directory (code ' + e.code + '): ' + dirPath); } } } +} - function getFreePort (): Promise { - return new Promise((resolve, reject) => { - const server = require('net').createServer(); - server.unref(); - server.on('error', reject); - server.listen(0, () => { - const port = server.address().port; - server.close(() => { - resolve(port); - }); +function getFreePort (): Promise { + return new Promise((resolve, reject) => { + const server = require('net').createServer(); + server.unref(); + server.on('error', reject); + server.listen(0, () => { + const port = server.address().port; + server.close(() => { + resolve(port); }); }); - } + }); +} - function collectErrorLog (error: any): string { - if (error && error.code && error.toString()) { - return error.toString(); - } else { - let result = [error, error.stack].join('\n'); - try { - const errorString = error.toString(); - if (errorString.indexOf('You may only yield a function') >= 0) { - result = 'Function is not yieldable. Did you forget to provide a generator or promise in rule file ? \nFAQ http://anyproxy.io/4.x/#faq'; - } - } catch (e) {} - return result - } - } - - function isFunc (source: object): boolean { - return source && Object.prototype.toString.call(source) === '[object Function]'; - }; - - /** - * @param {object} content - * @returns the size of the content - */ - function getByteSize (content: Buffer): number { - return Buffer.byteLength(content); - }; - - /* - * identify whether the - */ - function isIpDomain (domain: string): boolean { - if (!domain) { - return false; - } - const ipReg = /^\d+?\.\d+?\.\d+?\.\d+?$/; - - return ipReg.test(domain); - }; - - function execScriptSync (cmd: string): object { - let stdout, - status = 0; +function collectErrorLog (error: any): string { + if (error && error.code && error.toString()) { + return error.toString(); + } else { + let result = [error, error.stack].join('\n'); try { - stdout = execSync(cmd); - } catch (err) { - stdout = err.stdout; - status = err.status; - } + const errorString = error.toString(); + if (errorString.indexOf('You may only yield a function') >= 0) { + result = 'Function is not yieldable. Did you forget to provide a generator or promise in rule file ? \nFAQ http://anyproxy.io/4.x/#faq'; + } + } catch (e) {} + return result + } +} - return { - stdout: stdout.toString(), - status - }; - }; +function isFunc (source: object): boolean { + return source && Object.prototype.toString.call(source) === '[object Function]'; +}; - function guideToHomePage (): void { - logUtil.info('Refer to http://anyproxy.io for more detail'); +/** +* @param {object} content +* @returns the size of the content +*/ +function getByteSize (content: Buffer): number { + return Buffer.byteLength(content); +}; + +/* +* identify whether the +*/ +function isIpDomain (domain: string): boolean { + if (!domain) { + return false; + } + const ipReg = /^\d+?\.\d+?\.\d+?\.\d+?$/; + + return ipReg.test(domain); +}; + +function execScriptSync (cmd: string): object { + let stdout, + status = 0; + try { + stdout = execSync(cmd); + } catch (err) { + stdout = err.stdout; + status = err.status; + } + + return { + stdout: stdout.toString(), + status }; +}; + +function guideToHomePage (): void { + logUtil.info('Refer to http://anyproxy.io for more detail'); +}; const Util = { @@ -364,9 +364,9 @@ const Util = { deleteFolderContentsRecursive, execScriptSync, guideToHomePage, - formatDate -} + formatDate, +}; export default Util; -module.exports = Util; -// })(); \ No newline at end of file + + diff --git a/lib/webInterface.js b/lib/webInterface.js index b039fc9..003da6d 100644 --- a/lib/webInterface.js +++ b/lib/webInterface.js @@ -9,7 +9,7 @@ const express = require('express'), path = require('path'), events = require('events'), qrCode = require('qrcode-npm'), - util = require('./util'), + util = require('./util').default, certMgr = require('./certMgr'), wsServer = require('./wsServer'), juicer = require('juicer'), @@ -56,7 +56,7 @@ class webInterface extends events.EventEmitter { */ getServer() { const self = this; - const recorder = self.recorder; + const recorder = self.recorder; const ipAddress = ip.address(), // userRule = proxyInstance.proxyRule, webBasePath = 'web'; diff --git a/package.json b/package.json index 2865e1d..c17c4c4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ }, "devDependencies": { "@types/node": "^8.10.21", + "@types/ws": "^6.0.0", "antd": "^2.5.0", "autoprefixer": "^6.4.1", "babel-core": "^6.14.0", diff --git a/rule_sample/sample_modify_request_path.js b/rule_sample/sample_modify_request_path.js index 1df4aca..f72e5d7 100644 --- a/rule_sample/sample_modify_request_path.js +++ b/rule_sample/sample_modify_request_path.js @@ -9,14 +9,19 @@ module.exports = { *beforeSendRequest(requestDetail) { if (requestDetail.url.indexOf('https://httpbin.org/user-agent') === 0) { + console.info(requestDetail._req.connection._peername); const newRequestOptions = requestDetail.requestOptions; - requestDetail.protocol = 'http'; + requestDetail.protocol = 'https'; newRequestOptions.hostname = '127.0.0.1' - newRequestOptions.port = '8008'; - newRequestOptions.path = '/index.html'; + newRequestOptions.port = '3001'; + newRequestOptions.path = '/test'; newRequestOptions.method = 'GET'; return requestDetail; } + + if (requestDetail.url.indexOf('http://mobilegw.stable.alipay.net/mgw.htm') === 0) { + console.info(requestDetail.requestData.toString()) + } }, *beforeDealHttpsRequest(requestDetail) { return true; diff --git a/test.js b/test.js index abd6d8c..9698d85 100644 --- a/test.js +++ b/test.js @@ -1,7 +1,7 @@ const Jasmine = require('jasmine'); const jasmine = new Jasmine(); -const util = require('./lib/util'); +const util = require('./lib/util').default; const path = require('path'); const testTmpPath = path.join(__dirname, './test/temp'); diff --git a/test/spec_lib/util.js b/test/spec_lib/util.js index 8881df3..ec31cf3 100644 --- a/test/spec_lib/util.js +++ b/test/spec_lib/util.js @@ -2,7 +2,7 @@ * test for rule replaceOption rule * */ -const util = require('../../lib/util'); +const util = require('../../lib/util').default; describe('utils', () => { it('should get some free ports', done => { diff --git a/tsconfig.json b/tsconfig.json index 7e454fc..da6c1a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "./dist" ], "include": [ - "./lib/*", - "./bin/*" + "./lib/**/*", + "./bin/**/*" ] } diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..6635f17 --- /dev/null +++ b/tslint.json @@ -0,0 +1,48 @@ +{ + "extends": "tslint:recommended", + "rulesDirectory": [ + ], + "tslint.options": { + "project": "tsconfig.json", + "typeCheck": true + }, + "rules": { + "jsdoc-format": false, + "object-literal-sort-keys": false, + "quotemark": [true, "single"], + "ordered-imports": false, + "only-arrow-functions": false, + "no-reference": false, + "typedef": [ + true, + "call-signature", + "parameter", + "member-variable-declaration" + ], + "max-line-length": { + "options": [ + 140 + ] + }, + "new-parens": true, + "no-arg": true, + "no-bitwise": true, + "no-conditional-assignment": true, + "no-consecutive-blank-lines": false, + "no-unused-expression": [ + true, + "allow-fast-null-checks" + ], + "no-console": { + "severity": "warning", + "options": [ + "debug", + "info", + "log", + "time", + "timeEnd", + "trace" + ] + } + } +} \ No newline at end of file diff --git a/typings/index.d.ts b/typings/index.d.ts index fe83544..12a6a93 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,6 +1,21 @@ -declare interface AnyProxyConfig { - port: number + +/// + +declare module NodeJS { + interface Global { + _throttle: any; + } } + +declare interface AnyProxyConfig { + port: string; // 代理监听端口 + httpServerPort: string; // web server 的端口 + forceProxyHttps: boolean; + dangerouslyIgnoreUnauthorized: boolean; // 是否忽略https证书 + wsIntercept: boolean; // 是否代理websocket + chunkSizeThreshold: number; // throttle的配置 +} + declare interface AnyProxyRule { summary?: string, beforeSendRequest?: Function, @@ -8,4 +23,59 @@ declare interface AnyProxyRule { beforeDealHttpsRequest?: Function, onError?: Function, onConnectError?: Function +} + +declare namespace AnyProxyRecorder { + type ResponseHeader = any; // 暂时无法引入http模块,会导致 + export type WsResourceInfo = { + time: number, + message: string, + isToServer: boolean + }; + + export type ResourceInfo = { + wsMessages?: Array, + statusCode?: number, + resHeader?: ResponseHeader, + host?: string, + method?: string, + path?: string, + url?: string, + startTime?: number, + endTime?: number, + res?: { + statusCode: number, + headers: ResponseHeader + }, + resBody?: string, + length?: number + }; +} + +// The request detail object AnyProxy used internally +declare interface AnyProxyRequestDetail { + protocol?: string; + requestOptions?: any; + requestData?: Buffer; + url?: string; + _req?: any; + _directlyPassToRespond?: boolean; + response?: AnyProxyResponse; // exists when gen the response directly from request +} + +declare interface AnyProxyResponse { + statusCode: number, + header: OneLevelObjectType; + body: string | Buffer; + rawBody: Buffer; +} + +declare interface AnyProxyReponseDetail { + response: AnyProxyResponse, + _res: any; + _directlyPassToRespond?: boolean; // no need to send the respnose out +} + +declare interface OneLevelObjectType { + [key: string]: string | boolean | number } \ No newline at end of file