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.
271 lines
7.9 KiB
271 lines
7.9 KiB
|
|
/** |
|
* index.js |
|
* |
|
* a request API compatible with window.fetch |
|
*/ |
|
|
|
var parse_url = require('url').parse; |
|
var resolve_url = require('url').resolve; |
|
var http = require('http'); |
|
var https = require('https'); |
|
var zlib = require('zlib'); |
|
var stream = require('stream'); |
|
|
|
var Body = require('./lib/body'); |
|
var Response = require('./lib/response'); |
|
var Headers = require('./lib/headers'); |
|
var Request = require('./lib/request'); |
|
var FetchError = require('./lib/fetch-error'); |
|
|
|
// commonjs |
|
module.exports = Fetch; |
|
// es6 default export compatibility |
|
module.exports.default = module.exports; |
|
|
|
/** |
|
* Fetch class |
|
* |
|
* @param Mixed url Absolute url or Request instance |
|
* @param Object opts Fetch options |
|
* @return Promise |
|
*/ |
|
function Fetch(url, opts) { |
|
|
|
// allow call as function |
|
if (!(this instanceof Fetch)) |
|
return new Fetch(url, opts); |
|
|
|
// allow custom promise |
|
if (!Fetch.Promise) { |
|
throw new Error('native promise missing, set Fetch.Promise to your favorite alternative'); |
|
} |
|
|
|
Body.Promise = Fetch.Promise; |
|
|
|
var self = this; |
|
|
|
// wrap http.request into fetch |
|
return new Fetch.Promise(function(resolve, reject) { |
|
// build request object |
|
var options = new Request(url, opts); |
|
|
|
if (!options.protocol || !options.hostname) { |
|
throw new Error('only absolute urls are supported'); |
|
} |
|
|
|
if (options.protocol !== 'http:' && options.protocol !== 'https:') { |
|
throw new Error('only http(s) protocols are supported'); |
|
} |
|
|
|
var send; |
|
if (options.protocol === 'https:') { |
|
send = https.request; |
|
} else { |
|
send = http.request; |
|
} |
|
|
|
// normalize headers |
|
var headers = new Headers(options.headers); |
|
|
|
if (options.compress) { |
|
headers.set('accept-encoding', 'gzip,deflate'); |
|
} |
|
|
|
if (!headers.has('user-agent')) { |
|
headers.set('user-agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); |
|
} |
|
|
|
if (!headers.has('connection') && !options.agent) { |
|
headers.set('connection', 'close'); |
|
} |
|
|
|
if (!headers.has('accept')) { |
|
headers.set('accept', '*/*'); |
|
} |
|
|
|
// detect form data input from form-data module, this hack avoid the need to pass multipart header manually |
|
if (!headers.has('content-type') && options.body && typeof options.body.getBoundary === 'function') { |
|
headers.set('content-type', 'multipart/form-data; boundary=' + options.body.getBoundary()); |
|
} |
|
|
|
// bring node-fetch closer to browser behavior by setting content-length automatically |
|
if (!headers.has('content-length') && /post|put|patch|delete/i.test(options.method)) { |
|
if (typeof options.body === 'string') { |
|
headers.set('content-length', Buffer.byteLength(options.body)); |
|
// detect form data input from form-data module, this hack avoid the need to add content-length header manually |
|
} else if (options.body && typeof options.body.getLengthSync === 'function') { |
|
// for form-data 1.x |
|
if (options.body._lengthRetrievers && options.body._lengthRetrievers.length == 0) { |
|
headers.set('content-length', options.body.getLengthSync().toString()); |
|
// for form-data 2.x |
|
} else if (options.body.hasKnownLength && options.body.hasKnownLength()) { |
|
headers.set('content-length', options.body.getLengthSync().toString()); |
|
} |
|
// this is only necessary for older nodejs releases (before iojs merge) |
|
} else if (options.body === undefined || options.body === null) { |
|
headers.set('content-length', '0'); |
|
} |
|
} |
|
|
|
options.headers = headers.raw(); |
|
|
|
// http.request only support string as host header, this hack make custom host header possible |
|
if (options.headers.host) { |
|
options.headers.host = options.headers.host[0]; |
|
} |
|
|
|
// send request |
|
var req = send(options); |
|
var reqTimeout; |
|
|
|
if (options.timeout) { |
|
req.once('socket', function(socket) { |
|
reqTimeout = setTimeout(function() { |
|
req.abort(); |
|
reject(new FetchError('network timeout at: ' + options.url, 'request-timeout')); |
|
}, options.timeout); |
|
}); |
|
} |
|
|
|
req.on('error', function(err) { |
|
clearTimeout(reqTimeout); |
|
reject(new FetchError('request to ' + options.url + ' failed, reason: ' + err.message, 'system', err)); |
|
}); |
|
|
|
req.on('response', function(res) { |
|
clearTimeout(reqTimeout); |
|
|
|
// handle redirect |
|
if (self.isRedirect(res.statusCode) && options.redirect !== 'manual') { |
|
if (options.redirect === 'error') { |
|
reject(new FetchError('redirect mode is set to error: ' + options.url, 'no-redirect')); |
|
return; |
|
} |
|
|
|
if (options.counter >= options.follow) { |
|
reject(new FetchError('maximum redirect reached at: ' + options.url, 'max-redirect')); |
|
return; |
|
} |
|
|
|
if (!res.headers.location) { |
|
reject(new FetchError('redirect location header missing at: ' + options.url, 'invalid-redirect')); |
|
return; |
|
} |
|
|
|
// per fetch spec, for POST request with 301/302 response, or any request with 303 response, use GET when following redirect |
|
if (res.statusCode === 303 |
|
|| ((res.statusCode === 301 || res.statusCode === 302) && options.method === 'POST')) |
|
{ |
|
options.method = 'GET'; |
|
delete options.body; |
|
delete options.headers['content-length']; |
|
} |
|
|
|
options.counter++; |
|
|
|
resolve(Fetch(resolve_url(options.url, res.headers.location), options)); |
|
return; |
|
} |
|
|
|
// normalize location header for manual redirect mode |
|
var headers = new Headers(res.headers); |
|
if (options.redirect === 'manual' && headers.has('location')) { |
|
headers.set('location', resolve_url(options.url, headers.get('location'))); |
|
} |
|
|
|
// prepare response |
|
var body = res.pipe(new stream.PassThrough()); |
|
var response_options = { |
|
url: options.url |
|
, status: res.statusCode |
|
, statusText: res.statusMessage |
|
, headers: headers |
|
, size: options.size |
|
, timeout: options.timeout |
|
}; |
|
|
|
// response object |
|
var output; |
|
|
|
// in following scenarios we ignore compression support |
|
// 1. compression support is disabled |
|
// 2. HEAD request |
|
// 3. no content-encoding header |
|
// 4. no content response (204) |
|
// 5. content not modified response (304) |
|
if (!options.compress || options.method === 'HEAD' || !headers.has('content-encoding') || res.statusCode === 204 || res.statusCode === 304) { |
|
output = new Response(body, response_options); |
|
resolve(output); |
|
return; |
|
} |
|
|
|
// otherwise, check for gzip or deflate |
|
var name = headers.get('content-encoding'); |
|
|
|
// for gzip |
|
if (name == 'gzip' || name == 'x-gzip') { |
|
body = body.pipe(zlib.createGunzip()); |
|
output = new Response(body, response_options); |
|
resolve(output); |
|
return; |
|
|
|
// for deflate |
|
} else if (name == 'deflate' || name == 'x-deflate') { |
|
// handle the infamous raw deflate response from old servers |
|
// a hack for old IIS and Apache servers |
|
var raw = res.pipe(new stream.PassThrough()); |
|
raw.once('data', function(chunk) { |
|
// see http://stackoverflow.com/questions/37519828 |
|
if ((chunk[0] & 0x0F) === 0x08) { |
|
body = body.pipe(zlib.createInflate()); |
|
} else { |
|
body = body.pipe(zlib.createInflateRaw()); |
|
} |
|
output = new Response(body, response_options); |
|
resolve(output); |
|
}); |
|
return; |
|
} |
|
|
|
// otherwise, use response as-is |
|
output = new Response(body, response_options); |
|
resolve(output); |
|
return; |
|
}); |
|
|
|
// accept string, buffer or readable stream as body |
|
// per spec we will call tostring on non-stream objects |
|
if (typeof options.body === 'string') { |
|
req.write(options.body); |
|
req.end(); |
|
} else if (options.body instanceof Buffer) { |
|
req.write(options.body); |
|
req.end(); |
|
} else if (typeof options.body === 'object' && options.body.pipe) { |
|
options.body.pipe(req); |
|
} else if (typeof options.body === 'object') { |
|
req.write(options.body.toString()); |
|
req.end(); |
|
} else { |
|
req.end(); |
|
} |
|
}); |
|
|
|
}; |
|
|
|
/** |
|
* Redirect code matching |
|
* |
|
* @param Number code Status code |
|
* @return Boolean |
|
*/ |
|
Fetch.prototype.isRedirect = function(code) { |
|
return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; |
|
} |
|
|
|
// expose Promise |
|
Fetch.Promise = global.Promise; |
|
Fetch.Response = Response; |
|
Fetch.Headers = Headers; |
|
Fetch.Request = Request;
|
|
|