mirror of
https://github.com/avwo/whistle.git
synced 2026-10-18 22:51:32 +08:00
445 lines
10 KiB
JavaScript
445 lines
10 KiB
JavaScript
var Buffer = require('safe-buffer').Buffer;
|
|
var crypto = require('crypto');
|
|
var zlib = require('../util/zlib');
|
|
|
|
var STATUS_CODES = require('http').STATUS_CODES || {};
|
|
var wsParser = require('ws-parser');
|
|
|
|
var CRLF = Buffer.from('\r\n');
|
|
var TYPE_RE = /(request|response)-length:/i;
|
|
var frameIndex = 100000;
|
|
var TYPES = ['whistle', 'Fiddler', 'har'];
|
|
|
|
function dechunkify(body) {
|
|
var result = [];
|
|
var index;
|
|
while ((index = indexOfBuffer(body, CRLF)) > 0) {
|
|
var size = parseInt(body.slice(0, index).toString(), 16) || 0;
|
|
if (!size) {
|
|
break;
|
|
}
|
|
index += 2;
|
|
result.push(body.slice(index, (index += size)));
|
|
body = body.slice(index + 2);
|
|
}
|
|
return result.length ? Buffer.concat(result) : body;
|
|
}
|
|
|
|
function getMethod(method) {
|
|
if (typeof method !== 'string') {
|
|
return 'GET';
|
|
}
|
|
return method.trim().toUpperCase() || 'GET';
|
|
}
|
|
|
|
function getHeadersRaw(headers, rawHeaderNames) {
|
|
var result = [];
|
|
if (headers) {
|
|
rawHeaderNames = rawHeaderNames || {};
|
|
Object.keys(headers).forEach(function (name) {
|
|
var value = headers[name];
|
|
var key = rawHeaderNames[name] || name;
|
|
if (!Array.isArray(value)) {
|
|
result.push(key + ': ' + value);
|
|
return;
|
|
}
|
|
value.forEach(function (val) {
|
|
result.push(key + ': ' + val);
|
|
});
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function decodeRaw(headers, data) {
|
|
var body = getBodyBuffer(data);
|
|
var raw = Buffer.from(headers.join('\r\n') + '\r\n\r\n');
|
|
return body ? Buffer.concat([raw, body]) : raw;
|
|
}
|
|
|
|
function removeEncodingFields(headers) {
|
|
if (headers) {
|
|
delete headers['content-encoding'];
|
|
delete headers['transfer-encoding'];
|
|
}
|
|
}
|
|
|
|
function getBodyBuffer(data) {
|
|
if (data.base64) {
|
|
try {
|
|
return Buffer.from(data.base64 + '', 'base64');
|
|
} catch (e) {}
|
|
return Buffer.from(data.base64 + '');
|
|
}
|
|
if (data.body) {
|
|
return Buffer.from(data.body + '');
|
|
}
|
|
}
|
|
|
|
function getReqRaw(req) {
|
|
removeEncodingFields(req.headers);
|
|
var headers = getHeadersRaw(req.headers, req.rawHeaderNames);
|
|
var url = String(req.url || '').replace(/^ws/, 'http');
|
|
headers.unshift([getMethod(req.method), url, 'HTTP/1.1'].join(' '));
|
|
return decodeRaw(headers, req);
|
|
}
|
|
|
|
exports.getReqRaw = getReqRaw;
|
|
|
|
function getResRaw(res) {
|
|
removeEncodingFields(res.headers);
|
|
var headers = getHeadersRaw(res.headers, res.rawHeaderNames);
|
|
var statusCode = res.statusCode === 'aborted' ? 502 : res.statusCode;
|
|
var statusMessage = !statusCode
|
|
? ''
|
|
: res.statusMessage || STATUS_CODES[statusCode] || 'unknown';
|
|
headers.unshift(['HTTP/1.1', statusCode, statusMessage].join(' '));
|
|
return decodeRaw(headers, res);
|
|
}
|
|
|
|
exports.getResRaw = getResRaw;
|
|
|
|
var BODY_SEP = Buffer.from('\r\n\r\n');
|
|
|
|
function getBodyOffset(raw) {
|
|
var index = indexOfBuffer(raw, BODY_SEP);
|
|
if (index !== -1) {
|
|
return [index, index + 4];
|
|
}
|
|
}
|
|
function indexOfBuffer(buf, subBuf, start) {
|
|
start = start || 0;
|
|
if (buf.indexOf) {
|
|
return buf.indexOf(subBuf, start);
|
|
}
|
|
|
|
var subLen = subBuf.length;
|
|
if (subLen) {
|
|
for (var i = start, len = buf.length - subLen; i <= len; i++) {
|
|
var j = 0;
|
|
for (; j < subLen; j++) {
|
|
if (subBuf[j] !== buf[i + j]) {
|
|
break;
|
|
}
|
|
}
|
|
if (j == subLen) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
function getBody(body, headers, callback) {
|
|
if (body) {
|
|
var chunked = headers['transfer-encoding'];
|
|
if (typeof chunked === 'string') {
|
|
chunked = chunked.trim().toLowerCase();
|
|
}
|
|
if (chunked === 'chunked') {
|
|
body = dechunkify(body);
|
|
}
|
|
}
|
|
|
|
zlib.unzip(headers['content-encoding'], body, function (err, result) {
|
|
if (!err && result) {
|
|
body = result;
|
|
}
|
|
return callback(body && body.toString('base64'));
|
|
});
|
|
}
|
|
|
|
function parseRawData(raw, callback) {
|
|
var offset = getBodyOffset(raw);
|
|
var body = '';
|
|
if (offset) {
|
|
body = raw.slice(offset[1]);
|
|
raw = raw.slice(0, offset[0]);
|
|
}
|
|
raw = raw.toString();
|
|
raw = raw.trim().split(/\r\n?|\n/);
|
|
var statusLine = raw.shift().split(/\s+/);
|
|
var firstLine = statusLine.splice(0, 2);
|
|
firstLine[2] = statusLine.join(' ');
|
|
var headers = {};
|
|
var rawHeaderNames = {};
|
|
raw.forEach(function (line) {
|
|
var index = line.indexOf(':');
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
var name = line.substring(0, index).trim();
|
|
if (!name) {
|
|
return;
|
|
}
|
|
var key = name.toLowerCase();
|
|
var value = headers[key];
|
|
var val = line.substring(index + 1).trim();
|
|
if (value != null) {
|
|
if (Array.isArray(value)) {
|
|
value.push(val);
|
|
} else {
|
|
value = [value, val];
|
|
}
|
|
} else {
|
|
value = val;
|
|
}
|
|
rawHeaderNames[key] = name;
|
|
headers[key] = value;
|
|
});
|
|
|
|
getBody(body, headers, function (base64) {
|
|
callback({
|
|
firstLine: firstLine,
|
|
headers: headers,
|
|
size: base64 ? base64.length : 0,
|
|
rawHeaderNames: rawHeaderNames,
|
|
base64: base64
|
|
});
|
|
});
|
|
}
|
|
|
|
function getReq(raw, callback) {
|
|
raw = parseRawData(raw, function (raw) {
|
|
var method = raw.firstLine[0] || 'GET';
|
|
callback(
|
|
raw
|
|
? {
|
|
method: method,
|
|
httpVersion: '1.1',
|
|
rawHeaderNames: raw.rawHeaderNames,
|
|
url: raw.firstLine[1],
|
|
headers: raw.headers,
|
|
size: /^get$/i.test(method) ? 0 : raw.size,
|
|
base64: raw.base64
|
|
}
|
|
: null
|
|
);
|
|
});
|
|
}
|
|
|
|
exports.getReq = getReq;
|
|
|
|
function getRes(raw, callback) {
|
|
parseRawData(
|
|
raw,
|
|
function (raw) {
|
|
callback(
|
|
raw
|
|
? {
|
|
statusCode: raw.firstLine[1],
|
|
httpVersion: '1.1',
|
|
rawHeaderNames: raw.rawHeaderNames,
|
|
statusMessage: raw.firstLine[2],
|
|
headers: raw.headers,
|
|
size: raw.size,
|
|
base64: raw.base64
|
|
}
|
|
: {}
|
|
);
|
|
},
|
|
true
|
|
);
|
|
}
|
|
|
|
exports.getRes = getRes;
|
|
|
|
function parseJSON(str) {
|
|
try {
|
|
return JSON.parse(str);
|
|
} catch (e) {}
|
|
}
|
|
|
|
exports.parseJSON = parseJSON;
|
|
|
|
function padding(num) {
|
|
return num < 10 ? '0' + num : num;
|
|
}
|
|
|
|
function paddingMS(ms) {
|
|
if (ms > 99) {
|
|
return ms;
|
|
}
|
|
if (ms > 9) {
|
|
return '0' + ms;
|
|
}
|
|
return '00' + ms;
|
|
}
|
|
|
|
function formatDate() {
|
|
var date = new Date();
|
|
var result = [];
|
|
result.push(date.getFullYear());
|
|
result.push(padding(date.getMonth() + 1));
|
|
result.push(padding(date.getDate()));
|
|
result.push(padding(date.getHours()));
|
|
result.push(padding(date.getMinutes()));
|
|
result.push(padding(date.getSeconds()));
|
|
result.push(paddingMS(date.getMilliseconds()));
|
|
return result.join('');
|
|
}
|
|
|
|
function getFilename(type, filename) {
|
|
if (TYPES.indexOf(type) === -1) {
|
|
type = 'whistle';
|
|
}
|
|
if (typeof filename !== 'string') {
|
|
filename = '';
|
|
}
|
|
if (type === 'whistle') {
|
|
if (filename) {
|
|
if (!/\.(json|txt)$/i.test(filename)) {
|
|
filename += '.txt';
|
|
}
|
|
} else {
|
|
filename = 'whistle_' + formatDate() + '.txt';
|
|
}
|
|
} else if (type === 'har') {
|
|
if (filename) {
|
|
if (!/\.har$/i.test(filename)) {
|
|
filename += '.har';
|
|
}
|
|
} else {
|
|
filename = 'har_' + formatDate() + '.har';
|
|
}
|
|
} else {
|
|
if (filename) {
|
|
if (!/\.saz$/i.test(filename)) {
|
|
filename += '.saz';
|
|
}
|
|
} else {
|
|
filename = 'fiddler_' + formatDate() + '.saz';
|
|
}
|
|
}
|
|
return filename;
|
|
}
|
|
|
|
exports.getFilename = getFilename;
|
|
|
|
var ONE_MINUTE = 60 * 1000;
|
|
function toISOString(time) {
|
|
var date = new Date();
|
|
var offet = -date.getTimezoneOffset();
|
|
time += offet * ONE_MINUTE;
|
|
offet /= 60;
|
|
time = time >= 0 ? new Date(time) : new Date();
|
|
return (
|
|
time.toISOString().slice(0, -1) +
|
|
'0000' +
|
|
(offet >= 0 ? '+' : '-') +
|
|
padding(Math.abs(offet)) +
|
|
':00'
|
|
);
|
|
}
|
|
|
|
exports.toISOString = toISOString;
|
|
|
|
function removeIPV6Prefix(ip) {
|
|
if (typeof ip != 'string') {
|
|
return '';
|
|
}
|
|
|
|
return ip.indexOf('::ffff:') === 0 ? ip.substring(7) : ip;
|
|
}
|
|
|
|
exports.removeIPV6Prefix = removeIPV6Prefix;
|
|
|
|
function getIndex() {
|
|
if (frameIndex > 10000000) {
|
|
frameIndex = 100000;
|
|
}
|
|
return ++frameIndex;
|
|
}
|
|
|
|
function noop() {}
|
|
|
|
function resolveFrames(res, frames, callback) {
|
|
var len = frames.length;
|
|
var result = [];
|
|
if (!len) {
|
|
return callback(result);
|
|
}
|
|
res.headers = res.headers || {};
|
|
var receiver = wsParser.getReceiver(res);
|
|
var execCallback = function () {
|
|
if (receiver) {
|
|
receiver.onData = noop;
|
|
receiver = null;
|
|
callback(result);
|
|
}
|
|
};
|
|
var index = 0;
|
|
receiver.onerror = execCallback;
|
|
receiver.onclose = execCallback;
|
|
receiver.onData = function (chunk, opts) {
|
|
var frame = frames[index];
|
|
++index;
|
|
if (frame) {
|
|
result.push({
|
|
frameId: frame.frameId,
|
|
isClient: frame.type === 'request',
|
|
mask: opts.mask,
|
|
base64: chunk.toString('base64'),
|
|
compressed: opts.compressed,
|
|
length: opts.length,
|
|
unzipLen: chunk && opts.compressed ? chunk.length : undefined,
|
|
opcode: opts.opcode
|
|
});
|
|
}
|
|
if (!frame || len === index) {
|
|
setImmediate(execCallback);
|
|
}
|
|
};
|
|
setTimeout(execCallback, 3000);
|
|
frames.forEach((frame) => {
|
|
receiver.add(frame.bin);
|
|
});
|
|
}
|
|
|
|
function parseFrames(res, content, callback) {
|
|
var end = content.indexOf(CRLF, 0);
|
|
var start = 2;
|
|
var frames = [];
|
|
|
|
while (end !== -1) {
|
|
var line = content.slice(start, end).toString();
|
|
if (TYPE_RE.test(line)) {
|
|
var frame = { type: RegExp.$1.toLowerCase() };
|
|
frame.length = line.substring(line.indexOf(':') + 1).trim();
|
|
start = content.indexOf(CRLF, start + 90);
|
|
if (start === -1) {
|
|
break;
|
|
}
|
|
start += 2;
|
|
end = content.indexOf(CRLF, start);
|
|
if (end === -1) {
|
|
break;
|
|
}
|
|
line = content.slice(start, end).toString();
|
|
var time = new Date(
|
|
line.substring(line.indexOf(':') + 1).trim() || 0
|
|
).getTime();
|
|
frame.frameId = time + '-' + getIndex();
|
|
start = end + 4;
|
|
end = content.indexOf(CRLF, start);
|
|
if (end === -1) {
|
|
break;
|
|
}
|
|
frame.bin = content.slice(start, end);
|
|
frames.push(frame);
|
|
}
|
|
start = end + 2;
|
|
end = content.indexOf(CRLF, start);
|
|
}
|
|
resolveFrames(res, frames, callback);
|
|
}
|
|
|
|
exports.parseFrames = parseFrames;
|
|
|
|
exports.getHexHash = function(ctn, key) {
|
|
return crypto
|
|
.createHmac('sha256', key || 'whistle_temp_files')
|
|
.update(ctn || '').digest('hex').toLowerCase();
|
|
};
|