whistle 2.10.5 → 2.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/assets/menu.html +2 -2
  2. package/assets/modal.html +2 -2
  3. package/assets/tab.html +2 -2
  4. package/bin/api.js +545 -0
  5. package/bin/import.js +3 -3
  6. package/bin/plugin.js +4 -8
  7. package/bin/use.js +22 -8
  8. package/bin/util.js +12 -7
  9. package/bin/whistle.js +11 -1
  10. package/biz/webui/cgi-bin/abort.js +14 -5
  11. package/biz/webui/cgi-bin/frames.js +12 -0
  12. package/biz/webui/cgi-bin/get-session.js +7 -3
  13. package/biz/webui/cgi-bin/is-enabled-https.js +6 -0
  14. package/biz/webui/cgi-bin/plugins/disable-plugin.js +16 -7
  15. package/biz/webui/cgi-bin/plugins/list.js +17 -0
  16. package/biz/webui/cgi-bin/plugins/plugin.js +21 -0
  17. package/biz/webui/cgi-bin/plugins/status.js +21 -0
  18. package/biz/webui/cgi-bin/rules/is-multi-select.js +7 -0
  19. package/biz/webui/cgi-bin/rules/move-top.js +11 -0
  20. package/biz/webui/cgi-bin/rules/select.js +1 -1
  21. package/biz/webui/cgi-bin/rules/select2.js +16 -0
  22. package/biz/webui/cgi-bin/rules/status.js +6 -0
  23. package/biz/webui/cgi-bin/rules/unselect.js +3 -2
  24. package/biz/webui/cgi-bin/rules/unselect2.js +18 -0
  25. package/biz/webui/cgi-bin/rules/value.js +19 -0
  26. package/biz/webui/cgi-bin/sessions.js +6 -0
  27. package/biz/webui/cgi-bin/util.js +45 -20
  28. package/biz/webui/htdocs/index.html +1 -1
  29. package/biz/webui/htdocs/js/index.js +51 -51
  30. package/biz/webui/lib/index.js +1 -3
  31. package/lib/https/index.js +7 -5
  32. package/lib/init.js +1 -0
  33. package/lib/inspectors/data.js +39 -34
  34. package/lib/inspectors/req.js +9 -6
  35. package/lib/inspectors/res.js +10 -14
  36. package/lib/plugins/index.js +2 -2
  37. package/lib/plugins/load-plugin.js +19 -8
  38. package/lib/rules/util.js +20 -10
  39. package/lib/service/composer.js +7 -22
  40. package/lib/service/data-center.js +0 -12
  41. package/lib/service/generate-saz.js +2 -2
  42. package/lib/service/service.js +8 -7
  43. package/lib/service/util.js +6 -16
  44. package/lib/socket-mgr.js +23 -17
  45. package/lib/tunnel.js +7 -17
  46. package/lib/upgrade.js +4 -8
  47. package/lib/util/common.js +78 -11
  48. package/lib/util/data-server.js +273 -0
  49. package/lib/util/file-mgr.js +7 -4
  50. package/lib/util/http-mgr.js +17 -9
  51. package/lib/util/index.js +67 -54
  52. package/lib/util/parse-url.js +11 -9
  53. package/lib/util/patch.js +2 -6
  54. package/lib/util/speed-transform.js +17 -6
  55. package/lib/util/whistle-transform.js +18 -7
  56. package/package.json +10 -7
@@ -1,7 +1,6 @@
1
1
  var common = require('../util/common');
2
2
  var request = require('../util/http-mgr').request;
3
3
  var util = require('./util');
4
- var extend = require('extend');
5
4
 
6
5
  var requestData;
7
6
  var BASE_URL = 'https://' + common.SERVICE_HOST;
@@ -71,17 +70,6 @@ var saveComposeData = function(data) {
71
70
  if (!config.whistleToken || !Array.isArray(historyList)) {
72
71
  return;
73
72
  }
74
- historyList.map(function(item) {
75
- var newItem = extend({}, item);
76
- var rawData = item.headers + '\r\n\r\n' + (item.base64 || item.body || '');
77
- if (!item.dataHash && !item.base64Hash) {
78
- item[item.base64 ? 'base64Hash' : 'dataHash'] = util.getHexHash(rawData);
79
- }
80
- delete newItem.headers;
81
- delete newItem.body;
82
- delete newItem.base64;
83
- return newItem;
84
- });
85
73
  };
86
74
 
87
75
  var descSorter = function(a, b) {
@@ -124,11 +124,11 @@ module.exports = function (body, callback) {
124
124
  var zip = new AdmZip();
125
125
  sessions.map(function (item, index) {
126
126
  item.req.url = item.url;
127
- var req = util.getReqRaw(item.req);
127
+ var req = util.getRawReq(item.req);
128
128
  var res =
129
129
  !item.res || item.res.statusCode == null
130
130
  ? Buffer.from('')
131
- : util.getResRaw(item.res);
131
+ : util.getRawRes(item.res);
132
132
  var name = getName();
133
133
  item.index = index;
134
134
  zip.addFile('raw/' + name + '_c.txt', req);
@@ -96,12 +96,13 @@ function getFilename(filename, time, count) {
96
96
  }
97
97
 
98
98
  function saveSessions(data, cb) {
99
- var count = Array.isArray(data.sessions) && data.sessions.length;
99
+ var sessions = data.sessions;
100
+ var count = Array.isArray(sessions) && sessions.length;
100
101
  if (!count) {
101
102
  return cb();
102
103
  }
103
104
  limiter.push(function (done) {
104
- gzip(JSON.stringify(data.sessions), function(err, buf) {
105
+ gzip(JSON.stringify(sessions), function(err, buf) {
105
106
  done();
106
107
  if (err) {
107
108
  return cb(err);
@@ -109,7 +110,7 @@ function saveSessions(data, cb) {
109
110
  var filename = getFilename(data.filename, 0, count);
110
111
  common.writeFile(path.join(SAVED_SESSIONS_PATH, filename), buf, function(e) {
111
112
  !e && saveData({ type: 'savedData', filename: filename, buffer: buf } );
112
- cb(e);
113
+ cb(e, filename.substring(filename.indexOf('/') + 1));
113
114
  });
114
115
  });
115
116
  });
@@ -165,7 +166,7 @@ function getTempFiles(list, cb) {
165
166
  cb(result);
166
167
  }
167
168
  };
168
- list = list.forEach(function(filename) {
169
+ list.forEach(function(filename) {
169
170
  filename = common.getTempFile(filename);
170
171
  if (!filename) {
171
172
  return execCb();
@@ -415,8 +416,8 @@ function sessionsHandler() {
415
416
  res.json({ec: 0});
416
417
  });
417
418
  app.post('/cgi-bin/saved/save', jsonParser, function(req, res) {
418
- saveSessions(req.body, function(err) {
419
- res.json({ec: err ? 2 : 0, em: err ? err.message || 'Error' : undefined});
419
+ saveSessions(req.body, function(err, filename) {
420
+ res.json({ec: err ? 2 : 0, em: err ? err.message || 'Error' : undefined, filename: filename});
420
421
  });
421
422
  });
422
423
  app.post('/cgi-bin/composer', jsonParser, composer.handleRequest);
@@ -504,7 +505,7 @@ function sessionsHandler() {
504
505
  common.sendGzip(req, res, data);
505
506
  });
506
507
  } catch (e) {
507
- common.sendRes(req, 500, e.stack);
508
+ common.sendRes(res, 500, e.stack);
508
509
  }
509
510
  }
510
511
  );
@@ -77,7 +77,7 @@ function getBodyBuffer(data) {
77
77
  }
78
78
  }
79
79
 
80
- function getReqRaw(req) {
80
+ function getRawReq(req) {
81
81
  removeEncodingFields(req.headers);
82
82
  var headers = getHeadersRaw(req.headers, req.rawHeaderNames);
83
83
  var url = String(req.url || '').replace(/^ws/, 'http');
@@ -85,9 +85,9 @@ function getReqRaw(req) {
85
85
  return decodeRaw(headers, req);
86
86
  }
87
87
 
88
- exports.getReqRaw = getReqRaw;
88
+ exports.getRawReq = getRawReq;
89
89
 
90
- function getResRaw(res) {
90
+ function getRawRes(res) {
91
91
  removeEncodingFields(res.headers);
92
92
  var headers = getHeadersRaw(res.headers, res.rawHeaderNames);
93
93
  var statusCode = res.statusCode === 'aborted' ? 502 : res.statusCode;
@@ -98,7 +98,7 @@ function getResRaw(res) {
98
98
  return decodeRaw(headers, res);
99
99
  }
100
100
 
101
- exports.getResRaw = getResRaw;
101
+ exports.getRawRes = getRawRes;
102
102
 
103
103
  var BODY_SEP = Buffer.from('\r\n\r\n');
104
104
 
@@ -425,23 +425,13 @@ function parseFrames(res, content, callback) {
425
425
  exports.parseFrames = parseFrames;
426
426
 
427
427
  var MAX_SESSIONS_FILES = 2000;
428
- var SAVED_SESSIONS_FILE_RE = /_([1-9]\d*)_(\d+)$/;
429
428
  var DIR_RE = /^\d{6}$/;
430
429
 
431
- exports.SAVED_SESSIONS_FILE_RE = SAVED_SESSIONS_FILE_RE;
432
-
433
430
  function getFileList(files) {
434
431
  var result = [];
435
432
  files.forEach(function(file, i) {
436
- if (SAVED_SESSIONS_FILE_RE.test(file)) {
437
- var count = RegExp.$1;
438
- var time = RegExp.$2;
439
- result.push({
440
- filename: file.slice(0, - time.length - count.length - 2),
441
- count: +count,
442
- time: +time
443
- });
444
- }
433
+ file = common.getSavedFileItem(file);
434
+ file && result.push(file);
445
435
  });
446
436
  return result;
447
437
  }
package/lib/socket-mgr.js CHANGED
@@ -584,7 +584,7 @@ function handleWsReceive(ctx, resTrans, receiveStatus, handleTransform, frameCtx
584
584
 
585
585
  function getContext(req, res, hasEvent, sendStatus, receiveStatus, noDecompress) {
586
586
  var reqId = req.reqId;
587
- var ctx = (conns[reqId] = {
587
+ var ctx = conns[reqId] = {
588
588
  customParser: req.customParser,
589
589
  req: req,
590
590
  res: res,
@@ -601,7 +601,7 @@ function getContext(req, res, hasEvent, sendStatus, receiveStatus, noDecompress)
601
601
  setReceiveStatus: function (status) {
602
602
  setConnStatus(ctx, status, receiveStatus);
603
603
  }
604
- });
604
+ };
605
605
  initStatus(ctx, req.enable);
606
606
  return ctx;
607
607
  }
@@ -718,9 +718,21 @@ exports.handleUpgrade = function (req, res) {
718
718
  hasEvent && proxy.emit('wsRequest', url);
719
719
  cb(null, chunk);
720
720
  };
721
- if (hideFrame && !frameCtx) {
721
+ var ctx = false;
722
+ if (!hide || frameCtx) {
723
+ ctx = getContext(
724
+ req,
725
+ res,
726
+ hasEvent,
727
+ sendStatus,
728
+ receiveStatus,
729
+ noDecompress
730
+ );
731
+ ctx.hideFrame = req.hideFrame = hideFrame;
732
+ }
733
+ options.ctx = ctx;
734
+ if (!ctx || customParser) {
722
735
  reqTrans._transform = resTrans._transform = handleTransform;
723
- options.ctx = false;
724
736
  } else {
725
737
  reqTrans.headers = resTrans.headers = res.headers;
726
738
  if (reqWrite) {
@@ -731,15 +743,6 @@ exports.handleUpgrade = function (req, res) {
731
743
  resWrite.headers = res.headers;
732
744
  req.pipeWriter = resWrite;
733
745
  }
734
- var ctx = options.ctx = (conns[reqId] = getContext(
735
- req,
736
- res,
737
- hasEvent,
738
- sendStatus,
739
- receiveStatus,
740
- noDecompress
741
- ));
742
- ctx.hideFrame = req.hideFrame = hideFrame;
743
746
  handleWsSend(ctx, reqTrans, sendStatus, handleTransform, frameCtx);
744
747
  handleWsReceive(ctx, resTrans, receiveStatus, handleTransform, frameCtx);
745
748
  cleanCacheFrames(ctx, options);
@@ -798,9 +801,14 @@ exports.handleConnect = function (req, res, isUpgrade) {
798
801
  if (req._hasClosed) {
799
802
  return;
800
803
  }
801
- if (hideFrame && !frameCtx) {
804
+ var ctx = false;
805
+ if (!hide || frameCtx) {
806
+ ctx = getContext(req, res, hasEvent, sendStatus, receiveStatus);
807
+ ctx.hideFrame = req.hideFrame = hideFrame;
808
+ }
809
+ options.ctx = ctx;
810
+ if (!ctx || customParser) {
802
811
  reqTrans._transform = resTrans._transform = handleTransform;
803
- options.ctx = false;
804
812
  } else {
805
813
  req.wsExts = res.wsExts = reqTrans.wsExts = resTrans.wsExts = '';
806
814
  if (reqWrite) {
@@ -811,8 +819,6 @@ exports.handleConnect = function (req, res, isUpgrade) {
811
819
  resWrite.wsExts = '';
812
820
  req.pipeWriter = resWrite;
813
821
  }
814
- var ctx = options.ctx = getContext(req, res, hasEvent, sendStatus, receiveStatus);
815
- ctx.hideFrame = req.hideFrame = hideFrame;
816
822
  if (reqRead && reqWrite) {
817
823
  handleWsSend(ctx, reqTrans, sendStatus, handleTransform, frameCtx, eventName);
818
824
  } else {
package/lib/tunnel.js CHANGED
@@ -23,7 +23,6 @@ var X_RE = /^x/;
23
23
  var STATUS_CODES = require('http').STATUS_CODES || {};
24
24
  var getRawHeaderNames = hparser.getRawHeaderNames;
25
25
  var formatHeaders = hparser.formatHeaders;
26
- var getRawHeaders = hparser.getRawHeaders;
27
26
  var CRONET_RE = /\bCronet\b/;
28
27
  var SYS_HOST_RE = /^(?:[^/:]+\.)?(?:apple|cdn-apple|icloud|office|office365|mzstatic|apple-cloudkit|microsoft|parallels)\.(?:com|cn|com\.cn)$/;
29
28
  var TUNNEL_RE = /^tunnel:\/\//;
@@ -104,6 +103,7 @@ function tunnelProxy(server, proxy, type) {
104
103
  testId: req._testId,
105
104
  startTime: now,
106
105
  dnsTime: now,
106
+ servername: reqSocket.useSNI === hostname ? undefined : reqSocket.useSNI,
107
107
  captureError: true,
108
108
  rules: req.rules,
109
109
  req: {
@@ -382,7 +382,7 @@ function tunnelProxy(server, proxy, type) {
382
382
  var reqDelay = util.getMatcherValue(_rules.reqDelay);
383
383
  data.requestTime = data.dnsTime = Date.now();
384
384
  if (reqDelay > 0) {
385
- setTimeout(handleConnect, reqDelay);
385
+ util.setDelayTimer(reqSocket, setTimeout(handleConnect, reqDelay));
386
386
  } else {
387
387
  handleConnect();
388
388
  }
@@ -404,8 +404,7 @@ function tunnelProxy(server, proxy, type) {
404
404
  var proxyUrl;
405
405
  if (tunnelPort) {
406
406
  proxyUrl = 'proxy://127.0.0.1:' + tunnelPort;
407
- reqSocket.customParser = req.customParser =
408
- util.getParserStatus(req);
407
+ reqSocket.customParser = req.customParser = util.getParserStatus(req);
409
408
  pluginMgr.addSessionInfo(req, _rules);
410
409
  headers[config.PLUGIN_HOOK_NAME_HEADER] =
411
410
  config.PLUGIN_HOOKS.TUNNEL;
@@ -553,7 +552,7 @@ function tunnelProxy(server, proxy, type) {
553
552
  }
554
553
  };
555
554
  if (reqDelay > 0) {
556
- setTimeout(connectProxy, reqDelay);
555
+ util.setDelayTimer(reqSocket, setTimeout(connectProxy, reqDelay));
557
556
  } else {
558
557
  connectProxy();
559
558
  }
@@ -687,7 +686,7 @@ function tunnelProxy(server, proxy, type) {
687
686
  };
688
687
  var resDelay = util.getMatcherValue(_rules.resDelay);
689
688
  if (resDelay > 0) {
690
- setTimeout(handleEstablished, resDelay);
689
+ util.setDelayTimer(reqSocket, setTimeout(handleEstablished, resDelay));
691
690
  } else {
692
691
  handleEstablished();
693
692
  }
@@ -770,23 +769,14 @@ function tunnelProxy(server, proxy, type) {
770
769
  util.deleteResHeaders(req, resHeaders);
771
770
  code = util.getMatcherValue(reqRules.replaceStatus) || code;
772
771
  var isSuccess = code == 200;
773
- var message =
774
- isSuccess
775
- ? 'Connection Established'
776
- : STATUS_CODES[code] || 'unknown';
777
- var statusLine = ['HTTP/1.1', code, message].join(' ');
772
+ var message = isSuccess ? 'Connection Established' : STATUS_CODES[code] || 'unknown';
778
773
  var curHeaders = resHeaders;
779
- var rawData = statusLine;
780
774
  if (req.fromComposer) {
781
775
  curHeaders = extend({}, resHeaders);
782
776
  curHeaders['x-whistle-req-id'] = req.reqId;
783
777
  util.setFramesMode(curHeaders, isSuccess && inspect);
784
778
  }
785
- curHeaders = getRawHeaders(formatHeaders(curHeaders, rawHeaderNames));
786
- if (curHeaders) {
787
- rawData += '\r\n' + curHeaders;
788
- }
789
- rawData += '\r\n\r\n';
779
+ var rawData = common.getHttpMeta('HTTP/1.1', code, message, curHeaders, rawHeaderNames);
790
780
  try {
791
781
  if (code != 200) {
792
782
  reqSocket.end(rawData, cb);
package/lib/upgrade.js CHANGED
@@ -6,9 +6,7 @@ var util = require('./util');
6
6
  var config = require('./config');
7
7
  var common = require('./util/common');
8
8
 
9
- var formatHeaders = hparser.formatHeaders;
10
9
  var getRawHeaderNames = hparser.getRawHeaderNames;
11
- var getRawHeaders = hparser.getRawHeaders;
12
10
  var PLUGIN_PATH_RE =
13
11
  /^\/(\.\.\.whistle-path\.5b6af7b9884e1165\.\.\.\/\/\/)?(whistle|plugin)\.([^/?#]+)\/?/;
14
12
 
@@ -105,12 +103,10 @@ function upgradeHandler(req, socket) {
105
103
  util.addTunnelData(socket, headers);
106
104
  socket._clientId = util.getComposerClientId(headers);
107
105
  var getBuffer = function (method, newHeaders, path) {
108
- var rawData = (method || 'GET') + ' ' + (path || socket.url || req.url) + ' HTTP/1.1';
109
- newHeaders = formatHeaders(newHeaders || headers, req.rawHeaders);
110
- if (newHeaders = getRawHeaders(newHeaders)) {
111
- rawData += '\r\n' + newHeaders;
112
- }
113
- return Buffer.from(rawData + '\r\n\r\n');
106
+ method = method || 'GET';
107
+ path = path || socket.url || req.url;
108
+ newHeaders = newHeaders || headers;
109
+ return Buffer.from(common.getHttpMeta(method, path, 'HTTP/1.1', newHeaders, req.rawHeaders));
114
110
  };
115
111
  var sniPlugin = headers[util.SNI_PLUGIN_HEADER];
116
112
  if (sniPlugin) {
@@ -12,6 +12,7 @@ var json5 = require('json5');
12
12
  var extend = require('extend');
13
13
  var tls = require('tls');
14
14
  var https = require('https');
15
+ var hparser = require('hparser');
15
16
  var zlibx = require('./zlib');
16
17
  var pkgConf = require('../../package.json');
17
18
  var isUtf8 = require('./is-utf8');
@@ -72,10 +73,22 @@ var TOKEN = '\r\u0000\n\u0003\r';
72
73
  var PROP_SEP_RE = /(\\*)([|&]|\\[stnrfv])/g;
73
74
  var TEMP_FILE_RE = /^(?:\/?temp\/)?([\da-f]{64})(?:\.[\w.-]+)?$/;
74
75
  var TEMP_FILES_PATH = path.join(getWhistlePath(), 'temp_files');
76
+ var SAVED_SESSIONS_FILE_RE = /_([1-9]\d*)_(\d+)$/;
77
+
78
+ exports.getSavedFileItem = function(filename) {
79
+ var result = SAVED_SESSIONS_FILE_RE.exec(filename);
80
+ return result && {
81
+ count: +result[1],
82
+ time: +result[2],
83
+ filename: filename.slice(0, - result[1].length - result[2].length - 2)
84
+ };
85
+ };
75
86
 
76
87
 
77
88
  exports.WHISTLE_UID_HEADER = '_x-whistle-uid_';
78
89
 
90
+ exports.isUtf8 = isUtf8;
91
+
79
92
  exports.getTempFile = function(str) {
80
93
  var match = TEMP_FILE_RE.exec(str);
81
94
  return match && match[1];
@@ -670,18 +683,29 @@ exports.isGroup = function(name) {
670
683
  };
671
684
 
672
685
  var UTF8_RE = /^utf-?8$/i;
686
+
687
+ function getUtf8Buf(buffer, charset) {
688
+ charset = isString(charset) ? charset.trim() : '';
689
+ if (UTF8_RE.test(charset) || isUtf8(buffer)) {
690
+ return buffer;
691
+ }
692
+ try {
693
+ buffer = iconv.encode(buffer, charset || 'GB18030');
694
+ } catch (e) {}
695
+ return buffer;
696
+ }
697
+
698
+ exports.getUtf8Buf = getUtf8Buf;
699
+
700
+
673
701
  function bufferToString(buffer, encoding) {
674
- if (typeof encoding === 'string') {
675
- encoding = encoding.trim();
676
- } else {
677
- encoding = null;
702
+ if (!buffer) {
703
+ return '';
678
704
  }
679
- if (Buffer.isBuffer(buffer) && !UTF8_RE.test(encoding) && !isUtf8(buffer)) {
680
- try {
681
- buffer = iconv.encode(buffer, encoding || 'GB18030');
682
- } catch (e) {}
705
+ if (Buffer.isBuffer(buffer)) {
706
+ buffer = getUtf8Buf(buffer, encoding);
683
707
  }
684
- return String(buffer || '');
708
+ return String(buffer);
685
709
  }
686
710
 
687
711
  exports.bufferToString = bufferToString;
@@ -697,9 +721,9 @@ function isUrlEncoded(req) {
697
721
  exports.isUrlEncoded = isUrlEncoded;
698
722
 
699
723
  function readStream(stream, callback) {
700
- var buffer = null;
724
+ var buffer = [];
701
725
  stream.on('data', function(chunk) {
702
- buffer = buffer ? Buffer.concat([buffer, chunk]) : chunk;
726
+ buffer.push(chunk);
703
727
  });
704
728
  stream.once('end', function() {
705
729
  var buf;
@@ -708,6 +732,7 @@ function readStream(stream, callback) {
708
732
  var err;
709
733
  var jsonErr;
710
734
  stream.zlib = zlibx;
735
+ buffer = buffer.length > 1 ? Buffer.concat(buffer) : (buffer[0] || null);
711
736
  var encoding = stream.headers && stream.headers['content-encoding'];
712
737
  var getBuffer = function(cb) {
713
738
  if (err || buf !== undefined) {
@@ -851,9 +876,24 @@ exports.getHostPort = function(str) {
851
876
  }
852
877
  };
853
878
 
879
+ function clearDelayTimer(socket) {
880
+ var timer = socket._w2Timer_;
881
+ if (timer) {
882
+ clearTimeout(timer);
883
+ socket._w2Timer_ = null;
884
+ }
885
+ }
886
+
887
+ exports.clearDelayTimer = clearDelayTimer;
888
+ exports.setDelayTimer = function (socket, id) {
889
+ socket._w2Timer_ = id;
890
+ };
891
+
892
+
854
893
  exports.onSocketEnd = function(socket, callback) {
855
894
  var execCallback = function (err) {
856
895
  socket._hasError = true;
896
+ clearDelayTimer(socket);
857
897
  if (callback) {
858
898
  callback(err);
859
899
  callback = null;
@@ -969,6 +1009,10 @@ function isString(str) {
969
1009
 
970
1010
  exports.isString = isString;
971
1011
 
1012
+ exports.isStr = function(str) {
1013
+ return typeof str === 'string';
1014
+ };
1015
+
972
1016
  function isSafeNumStr(str) {
973
1017
  if (str == '0') {
974
1018
  return true;
@@ -1428,6 +1472,18 @@ exports.sendGzip = function (req, res, data) {
1428
1472
  });
1429
1473
  };
1430
1474
 
1475
+ exports.toString = function (str) {
1476
+ if (str != null) {
1477
+ if (typeof str === 'string') {
1478
+ return str;
1479
+ }
1480
+ try {
1481
+ return JSON.stringify(str) || '';
1482
+ } catch (e) {}
1483
+ }
1484
+ return '';
1485
+ };
1486
+
1431
1487
  exports.sendGzipText = function(req, res, headers, text, gzipText) {
1432
1488
  if (!text || !canGzip(req) || (!gzipText && text.length < GZIP_THRESHOLD)) {
1433
1489
  headers && res.writeHead(200, headers);
@@ -2045,3 +2101,14 @@ var COMMENT_RE = /#[^\r\n]*/g;
2045
2101
  exports.removeComment = function(text) {
2046
2102
  return text.replace(COMMENT_RE, '').trim();
2047
2103
  };
2104
+
2105
+ function getHttpMeta(f1, f2, f3, headers, rawHeaders) {
2106
+ var statusLine = [f1, f2, f3].join(' ');
2107
+ if (headers) {
2108
+ headers = hparser.formatHeaders(headers, rawHeaders);
2109
+ headers = hparser.getRawHeaders(headers);
2110
+ }
2111
+ return (headers ? statusLine + '\r\n' + headers : statusLine) + '\r\n\r\n';
2112
+ }
2113
+
2114
+ exports.getHttpMeta = getHttpMeta;