This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
5.4 KiB
261 lines
5.4 KiB
|
|
/** |
|
* body.js |
|
* |
|
* Body interface provides common methods for Request and Response |
|
*/ |
|
|
|
var convert = require('encoding').convert; |
|
var bodyStream = require('is-stream'); |
|
var PassThrough = require('stream').PassThrough; |
|
var FetchError = require('./fetch-error'); |
|
|
|
module.exports = Body; |
|
|
|
/** |
|
* Body class |
|
* |
|
* @param Stream body Readable stream |
|
* @param Object opts Response options |
|
* @return Void |
|
*/ |
|
function Body(body, opts) { |
|
|
|
opts = opts || {}; |
|
|
|
this.body = body; |
|
this.bodyUsed = false; |
|
this.size = opts.size || 0; |
|
this.timeout = opts.timeout || 0; |
|
this._raw = []; |
|
this._abort = false; |
|
|
|
} |
|
|
|
/** |
|
* Decode response as json |
|
* |
|
* @return Promise |
|
*/ |
|
Body.prototype.json = function() { |
|
|
|
var self = this; |
|
|
|
return this._decode().then(function(buffer) { |
|
try { |
|
return JSON.parse(buffer.toString()); |
|
} catch (err) { |
|
return Body.Promise.reject(new FetchError('invalid json response body at ' + self.url + ' reason: ' + err.message, 'invalid-json')); |
|
} |
|
}); |
|
|
|
}; |
|
|
|
/** |
|
* Decode response as text |
|
* |
|
* @return Promise |
|
*/ |
|
Body.prototype.text = function() { |
|
|
|
return this._decode().then(function(buffer) { |
|
return buffer.toString(); |
|
}); |
|
|
|
}; |
|
|
|
/** |
|
* Decode response as buffer (non-spec api) |
|
* |
|
* @return Promise |
|
*/ |
|
Body.prototype.buffer = function() { |
|
|
|
return this._decode(); |
|
|
|
}; |
|
|
|
/** |
|
* Decode buffers into utf-8 string |
|
* |
|
* @return Promise |
|
*/ |
|
Body.prototype._decode = function() { |
|
|
|
var self = this; |
|
|
|
if (this.bodyUsed) { |
|
return Body.Promise.reject(new Error('body used already for: ' + this.url)); |
|
} |
|
|
|
this.bodyUsed = true; |
|
this._bytes = 0; |
|
this._abort = false; |
|
this._raw = []; |
|
|
|
return new Body.Promise(function(resolve, reject) { |
|
var resTimeout; |
|
|
|
// body is string |
|
if (typeof self.body === 'string') { |
|
self._bytes = self.body.length; |
|
self._raw = [new Buffer(self.body)]; |
|
return resolve(self._convert()); |
|
} |
|
|
|
// body is buffer |
|
if (self.body instanceof Buffer) { |
|
self._bytes = self.body.length; |
|
self._raw = [self.body]; |
|
return resolve(self._convert()); |
|
} |
|
|
|
// allow timeout on slow response body |
|
if (self.timeout) { |
|
resTimeout = setTimeout(function() { |
|
self._abort = true; |
|
reject(new FetchError('response timeout at ' + self.url + ' over limit: ' + self.timeout, 'body-timeout')); |
|
}, self.timeout); |
|
} |
|
|
|
// handle stream error, such as incorrect content-encoding |
|
self.body.on('error', function(err) { |
|
reject(new FetchError('invalid response body at: ' + self.url + ' reason: ' + err.message, 'system', err)); |
|
}); |
|
|
|
// body is stream |
|
self.body.on('data', function(chunk) { |
|
if (self._abort || chunk === null) { |
|
return; |
|
} |
|
|
|
if (self.size && self._bytes + chunk.length > self.size) { |
|
self._abort = true; |
|
reject(new FetchError('content size at ' + self.url + ' over limit: ' + self.size, 'max-size')); |
|
return; |
|
} |
|
|
|
self._bytes += chunk.length; |
|
self._raw.push(chunk); |
|
}); |
|
|
|
self.body.on('end', function() { |
|
if (self._abort) { |
|
return; |
|
} |
|
|
|
clearTimeout(resTimeout); |
|
resolve(self._convert()); |
|
}); |
|
}); |
|
|
|
}; |
|
|
|
/** |
|
* Detect buffer encoding and convert to target encoding |
|
* ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding |
|
* |
|
* @param String encoding Target encoding |
|
* @return String |
|
*/ |
|
Body.prototype._convert = function(encoding) { |
|
|
|
encoding = encoding || 'utf-8'; |
|
|
|
var ct = this.headers.get('content-type'); |
|
var charset = 'utf-8'; |
|
var res, str; |
|
|
|
// header |
|
if (ct) { |
|
// skip encoding detection altogether if not html/xml/plain text |
|
if (!/text\/html|text\/plain|\+xml|\/xml/i.test(ct)) { |
|
return Buffer.concat(this._raw); |
|
} |
|
|
|
res = /charset=([^;]*)/i.exec(ct); |
|
} |
|
|
|
// no charset in content type, peek at response body for at most 1024 bytes |
|
if (!res && this._raw.length > 0) { |
|
for (var i = 0; i < this._raw.length; i++) { |
|
str += this._raw[i].toString() |
|
if (str.length > 1024) { |
|
break; |
|
} |
|
} |
|
str = str.substr(0, 1024); |
|
} |
|
|
|
// html5 |
|
if (!res && str) { |
|
res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str); |
|
} |
|
|
|
// html4 |
|
if (!res && str) { |
|
res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str); |
|
|
|
if (res) { |
|
res = /charset=(.*)/i.exec(res.pop()); |
|
} |
|
} |
|
|
|
// xml |
|
if (!res && str) { |
|
res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str); |
|
} |
|
|
|
// found charset |
|
if (res) { |
|
charset = res.pop(); |
|
|
|
// prevent decode issues when sites use incorrect encoding |
|
// ref: https://hsivonen.fi/encoding-menu/ |
|
if (charset === 'gb2312' || charset === 'gbk') { |
|
charset = 'gb18030'; |
|
} |
|
} |
|
|
|
// turn raw buffers into a single utf-8 buffer |
|
return convert( |
|
Buffer.concat(this._raw) |
|
, encoding |
|
, charset |
|
); |
|
|
|
}; |
|
|
|
/** |
|
* Clone body given Res/Req instance |
|
* |
|
* @param Mixed instance Response or Request instance |
|
* @return Mixed |
|
*/ |
|
Body.prototype._clone = function(instance) { |
|
var p1, p2; |
|
var body = instance.body; |
|
|
|
// don't allow cloning a used body |
|
if (instance.bodyUsed) { |
|
throw new Error('cannot clone body after it is used'); |
|
} |
|
|
|
// check that body is a stream and not form-data object |
|
// note: we can't clone the form-data object without having it as a dependency |
|
if (bodyStream(body) && typeof body.getBoundary !== 'function') { |
|
// tee instance body |
|
p1 = new PassThrough(); |
|
p2 = new PassThrough(); |
|
body.pipe(p1); |
|
body.pipe(p2); |
|
// set instance body to teed body and return the other teed body |
|
instance.body = p1; |
|
body = p2; |
|
} |
|
|
|
return body; |
|
} |
|
|
|
// expose Promise |
|
Body.Promise = global.Promise;
|
|
|