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.
1416 lines
34 KiB
1416 lines
34 KiB
'use strict'; |
|
|
|
Object.defineProperty(exports, '__esModule', { value: true }); |
|
|
|
// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js |
|
// (MIT licensed) |
|
|
|
const BUFFER = Symbol('buffer'); |
|
const TYPE = Symbol('type'); |
|
const CLOSED = Symbol('closed'); |
|
|
|
class Blob { |
|
constructor() { |
|
Object.defineProperty(this, Symbol.toStringTag, { |
|
value: 'Blob', |
|
writable: false, |
|
enumerable: false, |
|
configurable: true |
|
}); |
|
|
|
this[CLOSED] = false; |
|
this[TYPE] = ''; |
|
|
|
const blobParts = arguments[0]; |
|
const options = arguments[1]; |
|
|
|
const buffers = []; |
|
|
|
if (blobParts) { |
|
const a = blobParts; |
|
const length = Number(a.length); |
|
for (let i = 0; i < length; i++) { |
|
const element = a[i]; |
|
let buffer; |
|
if (element instanceof Buffer) { |
|
buffer = element; |
|
} else if (ArrayBuffer.isView(element)) { |
|
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); |
|
} else if (element instanceof ArrayBuffer) { |
|
buffer = Buffer.from(element); |
|
} else if (element instanceof Blob) { |
|
buffer = element[BUFFER]; |
|
} else { |
|
buffer = Buffer.from(typeof element === 'string' ? element : String(element)); |
|
} |
|
buffers.push(buffer); |
|
} |
|
} |
|
|
|
this[BUFFER] = Buffer.concat(buffers); |
|
|
|
let type = options && options.type !== undefined && String(options.type).toLowerCase(); |
|
if (type && !/[^\u0020-\u007E]/.test(type)) { |
|
this[TYPE] = type; |
|
} |
|
} |
|
get size() { |
|
return this[CLOSED] ? 0 : this[BUFFER].length; |
|
} |
|
get type() { |
|
return this[TYPE]; |
|
} |
|
get isClosed() { |
|
return this[CLOSED]; |
|
} |
|
slice() { |
|
const size = this.size; |
|
|
|
const start = arguments[0]; |
|
const end = arguments[1]; |
|
let relativeStart, relativeEnd; |
|
if (start === undefined) { |
|
relativeStart = 0; |
|
} else if (start < 0) { |
|
relativeStart = Math.max(size + start, 0); |
|
} else { |
|
relativeStart = Math.min(start, size); |
|
} |
|
if (end === undefined) { |
|
relativeEnd = size; |
|
} else if (end < 0) { |
|
relativeEnd = Math.max(size + end, 0); |
|
} else { |
|
relativeEnd = Math.min(end, size); |
|
} |
|
const span = Math.max(relativeEnd - relativeStart, 0); |
|
|
|
const buffer = this[BUFFER]; |
|
const slicedBuffer = buffer.slice(relativeStart, relativeStart + span); |
|
const blob = new Blob([], { type: arguments[2] }); |
|
blob[BUFFER] = slicedBuffer; |
|
blob[CLOSED] = this[CLOSED]; |
|
return blob; |
|
} |
|
close() { |
|
this[CLOSED] = true; |
|
} |
|
} |
|
|
|
Object.defineProperty(Blob.prototype, Symbol.toStringTag, { |
|
value: 'BlobPrototype', |
|
writable: false, |
|
enumerable: false, |
|
configurable: true |
|
}); |
|
|
|
/** |
|
* fetch-error.js |
|
* |
|
* FetchError interface for operational errors |
|
*/ |
|
|
|
/** |
|
* Create FetchError instance |
|
* |
|
* @param String message Error message for human |
|
* @param String type Error type for machine |
|
* @param String systemError For Node.js system error |
|
* @return FetchError |
|
*/ |
|
function FetchError(message, type, systemError) { |
|
Error.call(this, message); |
|
|
|
this.message = message; |
|
this.type = type; |
|
|
|
// when err.type is `system`, err.code contains system error code |
|
if (systemError) { |
|
this.code = this.errno = systemError.code; |
|
} |
|
|
|
// hide custom error implementation details from end-users |
|
Error.captureStackTrace(this, this.constructor); |
|
} |
|
|
|
FetchError.prototype = Object.create(Error.prototype); |
|
FetchError.prototype.constructor = FetchError; |
|
FetchError.prototype.name = 'FetchError'; |
|
|
|
/** |
|
* body.js |
|
* |
|
* Body interface provides common methods for Request and Response |
|
*/ |
|
|
|
const Stream = require('stream'); |
|
|
|
var _require$1 = require('stream'); |
|
|
|
const PassThrough$1 = _require$1.PassThrough; |
|
|
|
|
|
const DISTURBED = Symbol('disturbed'); |
|
|
|
let convert; |
|
try { |
|
convert = require('encoding').convert; |
|
} catch (e) {} |
|
|
|
/** |
|
* Body class |
|
* |
|
* Cannot use ES6 class because Body must be called with .call(). |
|
* |
|
* @param Stream body Readable stream |
|
* @param Object opts Response options |
|
* @return Void |
|
*/ |
|
function Body(body) { |
|
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, |
|
_ref$size = _ref.size; |
|
|
|
let size = _ref$size === undefined ? 0 : _ref$size; |
|
var _ref$timeout = _ref.timeout; |
|
let timeout = _ref$timeout === undefined ? 0 : _ref$timeout; |
|
|
|
if (body == null) { |
|
// body is undefined or null |
|
body = null; |
|
} else if (typeof body === 'string') { |
|
// body is string |
|
} else if (isURLSearchParams(body)) { |
|
// body is a URLSearchParams |
|
} else if (body instanceof Blob) { |
|
// body is blob |
|
} else if (Buffer.isBuffer(body)) { |
|
// body is buffer |
|
} else if (body instanceof Stream) { |
|
// body is stream |
|
} else { |
|
// none of the above |
|
// coerce to string |
|
body = String(body); |
|
} |
|
this.body = body; |
|
this[DISTURBED] = false; |
|
this.size = size; |
|
this.timeout = timeout; |
|
} |
|
|
|
Body.prototype = { |
|
get bodyUsed() { |
|
return this[DISTURBED]; |
|
}, |
|
|
|
/** |
|
* Decode response as ArrayBuffer |
|
* |
|
* @return Promise |
|
*/ |
|
arrayBuffer() { |
|
return consumeBody.call(this).then(function (buf) { |
|
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); |
|
}); |
|
}, |
|
|
|
/** |
|
* Return raw response as Blob |
|
* |
|
* @return Promise |
|
*/ |
|
blob() { |
|
let ct = this.headers && this.headers.get('content-type') || ''; |
|
return consumeBody.call(this).then(function (buf) { |
|
return Object.assign( |
|
// Prevent copying |
|
new Blob([], { |
|
type: ct.toLowerCase() |
|
}), { |
|
[BUFFER]: buf |
|
}); |
|
}); |
|
}, |
|
|
|
/** |
|
* Decode response as json |
|
* |
|
* @return Promise |
|
*/ |
|
json() { |
|
var _this = this; |
|
|
|
return consumeBody.call(this).then(function (buffer) { |
|
try { |
|
return JSON.parse(buffer.toString()); |
|
} catch (err) { |
|
return Body.Promise.reject(new FetchError(`invalid json response body at ${_this.url} reason: ${err.message}`, 'invalid-json')); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Decode response as text |
|
* |
|
* @return Promise |
|
*/ |
|
text() { |
|
return consumeBody.call(this).then(function (buffer) { |
|
return buffer.toString(); |
|
}); |
|
}, |
|
|
|
/** |
|
* Decode response as buffer (non-spec api) |
|
* |
|
* @return Promise |
|
*/ |
|
buffer() { |
|
return consumeBody.call(this); |
|
}, |
|
|
|
/** |
|
* Decode response as text, while automatically detecting the encoding and |
|
* trying to decode to UTF-8 (non-spec api) |
|
* |
|
* @return Promise |
|
*/ |
|
textConverted() { |
|
var _this2 = this; |
|
|
|
return consumeBody.call(this).then(function (buffer) { |
|
return convertBody(buffer, _this2.headers); |
|
}); |
|
} |
|
|
|
}; |
|
|
|
Body.mixIn = function (proto) { |
|
for (const name of Object.getOwnPropertyNames(Body.prototype)) { |
|
// istanbul ignore else: future proof |
|
if (!(name in proto)) { |
|
const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); |
|
Object.defineProperty(proto, name, desc); |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* Decode buffers into utf-8 string |
|
* |
|
* @return Promise |
|
*/ |
|
function consumeBody(body) { |
|
var _this3 = this; |
|
|
|
if (this[DISTURBED]) { |
|
return Body.Promise.reject(new Error(`body used already for: ${this.url}`)); |
|
} |
|
|
|
this[DISTURBED] = true; |
|
|
|
// body is null |
|
if (this.body === null) { |
|
return Body.Promise.resolve(Buffer.alloc(0)); |
|
} |
|
|
|
// body is string |
|
if (typeof this.body === 'string') { |
|
return Body.Promise.resolve(Buffer.from(this.body)); |
|
} |
|
|
|
// body is blob |
|
if (this.body instanceof Blob) { |
|
return Body.Promise.resolve(this.body[BUFFER]); |
|
} |
|
|
|
// body is buffer |
|
if (Buffer.isBuffer(this.body)) { |
|
return Body.Promise.resolve(this.body); |
|
} |
|
|
|
// istanbul ignore if: should never happen |
|
if (!(this.body instanceof Stream)) { |
|
return Body.Promise.resolve(Buffer.alloc(0)); |
|
} |
|
|
|
// body is stream |
|
// get ready to actually consume the body |
|
let accum = []; |
|
let accumBytes = 0; |
|
let abort = false; |
|
|
|
return new Body.Promise(function (resolve, reject) { |
|
let resTimeout; |
|
|
|
// allow timeout on slow response body |
|
if (_this3.timeout) { |
|
resTimeout = setTimeout(function () { |
|
abort = true; |
|
reject(new FetchError(`Response timeout while trying to fetch ${_this3.url} (over ${_this3.timeout}ms)`, 'body-timeout')); |
|
}, _this3.timeout); |
|
} |
|
|
|
// handle stream error, such as incorrect content-encoding |
|
_this3.body.on('error', function (err) { |
|
reject(new FetchError(`Invalid response body while trying to fetch ${_this3.url}: ${err.message}`, 'system', err)); |
|
}); |
|
|
|
_this3.body.on('data', function (chunk) { |
|
if (abort || chunk === null) { |
|
return; |
|
} |
|
|
|
if (_this3.size && accumBytes + chunk.length > _this3.size) { |
|
abort = true; |
|
reject(new FetchError(`content size at ${_this3.url} over limit: ${_this3.size}`, 'max-size')); |
|
return; |
|
} |
|
|
|
accumBytes += chunk.length; |
|
accum.push(chunk); |
|
}); |
|
|
|
_this3.body.on('end', function () { |
|
if (abort) { |
|
return; |
|
} |
|
|
|
clearTimeout(resTimeout); |
|
resolve(Buffer.concat(accum)); |
|
}); |
|
}); |
|
} |
|
|
|
/** |
|
* 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 Buffer buffer Incoming buffer |
|
* @param String encoding Target encoding |
|
* @return String |
|
*/ |
|
function convertBody(buffer, headers) { |
|
if (typeof convert !== 'function') { |
|
throw new Error('The package `encoding` must be installed to use the textConverted() function'); |
|
} |
|
|
|
const ct = headers.get('content-type'); |
|
let charset = 'utf-8'; |
|
let res, str; |
|
|
|
// header |
|
if (ct) { |
|
res = /charset=([^;]*)/i.exec(ct); |
|
} |
|
|
|
// no charset in content type, peek at response body for at most 1024 bytes |
|
str = buffer.slice(0, 1024).toString(); |
|
|
|
// 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, 'UTF-8', charset).toString(); |
|
} |
|
|
|
/** |
|
* Detect a URLSearchParams object |
|
* ref: https://github.com/bitinn/node-fetch/issues/296#issuecomment-307598143 |
|
* |
|
* @param Object obj Object to detect by type or brand |
|
* @return String |
|
*/ |
|
function isURLSearchParams(obj) { |
|
// Duck-typing as a necessary condition. |
|
if (typeof obj !== 'object' || typeof obj.append !== 'function' || typeof obj.delete !== 'function' || typeof obj.get !== 'function' || typeof obj.getAll !== 'function' || typeof obj.has !== 'function' || typeof obj.set !== 'function') { |
|
return false; |
|
} |
|
|
|
// Brand-checking and more duck-typing as optional condition. |
|
return obj.constructor.name === 'URLSearchParams' || Object.prototype.toString.call(obj) === '[object URLSearchParams]' || typeof obj.sort === 'function'; |
|
} |
|
|
|
/** |
|
* Clone body given Res/Req instance |
|
* |
|
* @param Mixed instance Response or Request instance |
|
* @return Mixed |
|
*/ |
|
function clone(instance) { |
|
let p1, p2; |
|
let 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 (body instanceof Stream && typeof body.getBoundary !== 'function') { |
|
// tee instance body |
|
p1 = new PassThrough$1(); |
|
p2 = new PassThrough$1(); |
|
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; |
|
} |
|
|
|
/** |
|
* Performs the operation "extract a `Content-Type` value from |object|" as |
|
* specified in the specification: |
|
* https://fetch.spec.whatwg.org/#concept-bodyinit-extract |
|
* |
|
* This function assumes that instance.body is present and non-null. |
|
* |
|
* @param Mixed instance Response or Request instance |
|
*/ |
|
function extractContentType(instance) { |
|
const body = instance.body; |
|
|
|
// istanbul ignore if: Currently, because of a guard in Request, body |
|
// can never be null. Included here for completeness. |
|
|
|
if (body === null) { |
|
// body is null |
|
return null; |
|
} else if (typeof body === 'string') { |
|
// body is string |
|
return 'text/plain;charset=UTF-8'; |
|
} else if (isURLSearchParams(body)) { |
|
// body is a URLSearchParams |
|
return 'application/x-www-form-urlencoded;charset=UTF-8'; |
|
} else if (body instanceof Blob) { |
|
// body is blob |
|
return body.type || null; |
|
} else if (Buffer.isBuffer(body)) { |
|
// body is buffer |
|
return null; |
|
} else if (typeof body.getBoundary === 'function') { |
|
// detect form data input from form-data module |
|
return `multipart/form-data;boundary=${body.getBoundary()}`; |
|
} else { |
|
// body is stream |
|
// can't really do much about this |
|
return null; |
|
} |
|
} |
|
|
|
function getTotalBytes(instance) { |
|
const body = instance.body; |
|
|
|
// istanbul ignore if: included for completion |
|
|
|
if (body === null) { |
|
// body is null |
|
return 0; |
|
} else if (typeof body === 'string') { |
|
// body is string |
|
return Buffer.byteLength(body); |
|
} else if (isURLSearchParams(body)) { |
|
// body is URLSearchParams |
|
return Buffer.byteLength(String(body)); |
|
} else if (body instanceof Blob) { |
|
// body is blob |
|
return body.size; |
|
} else if (Buffer.isBuffer(body)) { |
|
// body is buffer |
|
return body.length; |
|
} else if (body && typeof body.getLengthSync === 'function') { |
|
// detect form data input from form-data module |
|
if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x |
|
body.hasKnownLength && body.hasKnownLength()) { |
|
// 2.x |
|
return body.getLengthSync(); |
|
} |
|
return null; |
|
} else { |
|
// body is stream |
|
// can't really do much about this |
|
return null; |
|
} |
|
} |
|
|
|
function writeToStream(dest, instance) { |
|
const body = instance.body; |
|
|
|
|
|
if (body === null) { |
|
// body is null |
|
dest.end(); |
|
} else if (typeof body === 'string') { |
|
// body is string |
|
dest.write(body); |
|
dest.end(); |
|
} else if (isURLSearchParams(body)) { |
|
// body is URLSearchParams |
|
dest.write(Buffer.from(String(body))); |
|
dest.end(); |
|
} else if (body instanceof Blob) { |
|
// body is blob |
|
dest.write(body[BUFFER]); |
|
dest.end(); |
|
} else if (Buffer.isBuffer(body)) { |
|
// body is buffer |
|
dest.write(body); |
|
dest.end(); |
|
} else { |
|
// body is stream |
|
body.pipe(dest); |
|
} |
|
} |
|
|
|
// expose Promise |
|
Body.Promise = global.Promise; |
|
|
|
/** |
|
* A set of utilities borrowed from Node.js' _http_common.js |
|
*/ |
|
|
|
/** |
|
* Verifies that the given val is a valid HTTP token |
|
* per the rules defined in RFC 7230 |
|
* See https://tools.ietf.org/html/rfc7230#section-3.2.6 |
|
* |
|
* Allowed characters in an HTTP token: |
|
* ^_`a-z 94-122 |
|
* A-Z 65-90 |
|
* - 45 |
|
* 0-9 48-57 |
|
* ! 33 |
|
* #$%&' 35-39 |
|
* *+ 42-43 |
|
* . 46 |
|
* | 124 |
|
* ~ 126 |
|
* |
|
* This implementation of checkIsHttpToken() loops over the string instead of |
|
* using a regular expression since the former is up to 180% faster with v8 4.9 |
|
* depending on the string length (the shorter the string, the larger the |
|
* performance difference) |
|
* |
|
* Additionally, checkIsHttpToken() is currently designed to be inlinable by v8, |
|
* so take care when making changes to the implementation so that the source |
|
* code size does not exceed v8's default max_inlined_source_size setting. |
|
**/ |
|
/* istanbul ignore next */ |
|
function isValidTokenChar(ch) { |
|
if (ch >= 94 && ch <= 122) return true; |
|
if (ch >= 65 && ch <= 90) return true; |
|
if (ch === 45) return true; |
|
if (ch >= 48 && ch <= 57) return true; |
|
if (ch === 34 || ch === 40 || ch === 41 || ch === 44) return false; |
|
if (ch >= 33 && ch <= 46) return true; |
|
if (ch === 124 || ch === 126) return true; |
|
return false; |
|
} |
|
/* istanbul ignore next */ |
|
function checkIsHttpToken(val) { |
|
if (typeof val !== 'string' || val.length === 0) return false; |
|
if (!isValidTokenChar(val.charCodeAt(0))) return false; |
|
const len = val.length; |
|
if (len > 1) { |
|
if (!isValidTokenChar(val.charCodeAt(1))) return false; |
|
if (len > 2) { |
|
if (!isValidTokenChar(val.charCodeAt(2))) return false; |
|
if (len > 3) { |
|
if (!isValidTokenChar(val.charCodeAt(3))) return false; |
|
for (var i = 4; i < len; i++) { |
|
if (!isValidTokenChar(val.charCodeAt(i))) return false; |
|
} |
|
} |
|
} |
|
} |
|
return true; |
|
} |
|
/** |
|
* True if val contains an invalid field-vchar |
|
* field-value = *( field-content / obs-fold ) |
|
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] |
|
* field-vchar = VCHAR / obs-text |
|
* |
|
* checkInvalidHeaderChar() is currently designed to be inlinable by v8, |
|
* so take care when making changes to the implementation so that the source |
|
* code size does not exceed v8's default max_inlined_source_size setting. |
|
**/ |
|
/* istanbul ignore next */ |
|
function checkInvalidHeaderChar(val) { |
|
val += ''; |
|
if (val.length < 1) return false; |
|
var c = val.charCodeAt(0); |
|
if (c <= 31 && c !== 9 || c > 255 || c === 127) return true; |
|
if (val.length < 2) return false; |
|
c = val.charCodeAt(1); |
|
if (c <= 31 && c !== 9 || c > 255 || c === 127) return true; |
|
if (val.length < 3) return false; |
|
c = val.charCodeAt(2); |
|
if (c <= 31 && c !== 9 || c > 255 || c === 127) return true; |
|
for (var i = 3; i < val.length; ++i) { |
|
c = val.charCodeAt(i); |
|
if (c <= 31 && c !== 9 || c > 255 || c === 127) return true; |
|
} |
|
return false; |
|
} |
|
|
|
/** |
|
* headers.js |
|
* |
|
* Headers class offers convenient helpers |
|
*/ |
|
|
|
function sanitizeName(name) { |
|
name += ''; |
|
if (!checkIsHttpToken(name)) { |
|
throw new TypeError(`${name} is not a legal HTTP header name`); |
|
} |
|
return name.toLowerCase(); |
|
} |
|
|
|
function sanitizeValue(value) { |
|
value += ''; |
|
if (checkInvalidHeaderChar(value)) { |
|
throw new TypeError(`${value} is not a legal HTTP header value`); |
|
} |
|
return value; |
|
} |
|
|
|
const MAP = Symbol('map'); |
|
class Headers { |
|
/** |
|
* Headers class |
|
* |
|
* @param Object headers Response headers |
|
* @return Void |
|
*/ |
|
constructor() { |
|
let init = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; |
|
|
|
this[MAP] = Object.create(null); |
|
|
|
if (init instanceof Headers) { |
|
const rawHeaders = init.raw(); |
|
const headerNames = Object.keys(rawHeaders); |
|
|
|
for (const headerName of headerNames) { |
|
for (const value of rawHeaders[headerName]) { |
|
this.append(headerName, value); |
|
} |
|
} |
|
|
|
return; |
|
} |
|
|
|
// We don't worry about converting prop to ByteString here as append() |
|
// will handle it. |
|
if (init == null) { |
|
// no op |
|
} else if (typeof init === 'object') { |
|
const method = init[Symbol.iterator]; |
|
if (method != null) { |
|
if (typeof method !== 'function') { |
|
throw new TypeError('Header pairs must be iterable'); |
|
} |
|
|
|
// sequence<sequence<ByteString>> |
|
// Note: per spec we have to first exhaust the lists then process them |
|
const pairs = []; |
|
for (const pair of init) { |
|
if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { |
|
throw new TypeError('Each header pair must be iterable'); |
|
} |
|
pairs.push(Array.from(pair)); |
|
} |
|
|
|
for (const pair of pairs) { |
|
if (pair.length !== 2) { |
|
throw new TypeError('Each header pair must be a name/value tuple'); |
|
} |
|
this.append(pair[0], pair[1]); |
|
} |
|
} else { |
|
// record<ByteString, ByteString> |
|
for (const key of Object.keys(init)) { |
|
const value = init[key]; |
|
this.append(key, value); |
|
} |
|
} |
|
} else { |
|
throw new TypeError('Provided initializer must be an object'); |
|
} |
|
|
|
Object.defineProperty(this, Symbol.toStringTag, { |
|
value: 'Headers', |
|
writable: false, |
|
enumerable: false, |
|
configurable: true |
|
}); |
|
} |
|
|
|
/** |
|
* Return first header value given name |
|
* |
|
* @param String name Header name |
|
* @return Mixed |
|
*/ |
|
get(name) { |
|
const list = this[MAP][sanitizeName(name)]; |
|
if (!list) { |
|
return null; |
|
} |
|
|
|
return list.join(', '); |
|
} |
|
|
|
/** |
|
* Iterate over all headers |
|
* |
|
* @param Function callback Executed for each item with parameters (value, name, thisArg) |
|
* @param Boolean thisArg `this` context for callback function |
|
* @return Void |
|
*/ |
|
forEach(callback) { |
|
let thisArg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; |
|
|
|
let pairs = getHeaderPairs(this); |
|
let i = 0; |
|
while (i < pairs.length) { |
|
var _pairs$i = pairs[i]; |
|
const name = _pairs$i[0], |
|
value = _pairs$i[1]; |
|
|
|
callback.call(thisArg, value, name, this); |
|
pairs = getHeaderPairs(this); |
|
i++; |
|
} |
|
} |
|
|
|
/** |
|
* Overwrite header values given name |
|
* |
|
* @param String name Header name |
|
* @param String value Header value |
|
* @return Void |
|
*/ |
|
set(name, value) { |
|
this[MAP][sanitizeName(name)] = [sanitizeValue(value)]; |
|
} |
|
|
|
/** |
|
* Append a value onto existing header |
|
* |
|
* @param String name Header name |
|
* @param String value Header value |
|
* @return Void |
|
*/ |
|
append(name, value) { |
|
if (!this.has(name)) { |
|
this.set(name, value); |
|
return; |
|
} |
|
|
|
this[MAP][sanitizeName(name)].push(sanitizeValue(value)); |
|
} |
|
|
|
/** |
|
* Check for header name existence |
|
* |
|
* @param String name Header name |
|
* @return Boolean |
|
*/ |
|
has(name) { |
|
return !!this[MAP][sanitizeName(name)]; |
|
} |
|
|
|
/** |
|
* Delete all header values given name |
|
* |
|
* @param String name Header name |
|
* @return Void |
|
*/ |
|
delete(name) { |
|
delete this[MAP][sanitizeName(name)]; |
|
} |
|
|
|
/** |
|
* Return raw headers (non-spec api) |
|
* |
|
* @return Object |
|
*/ |
|
raw() { |
|
return this[MAP]; |
|
} |
|
|
|
/** |
|
* Get an iterator on keys. |
|
* |
|
* @return Iterator |
|
*/ |
|
keys() { |
|
return createHeadersIterator(this, 'key'); |
|
} |
|
|
|
/** |
|
* Get an iterator on values. |
|
* |
|
* @return Iterator |
|
*/ |
|
values() { |
|
return createHeadersIterator(this, 'value'); |
|
} |
|
|
|
/** |
|
* Get an iterator on entries. |
|
* |
|
* This is the default iterator of the Headers object. |
|
* |
|
* @return Iterator |
|
*/ |
|
[Symbol.iterator]() { |
|
return createHeadersIterator(this, 'key+value'); |
|
} |
|
} |
|
Headers.prototype.entries = Headers.prototype[Symbol.iterator]; |
|
|
|
Object.defineProperty(Headers.prototype, Symbol.toStringTag, { |
|
value: 'HeadersPrototype', |
|
writable: false, |
|
enumerable: false, |
|
configurable: true |
|
}); |
|
|
|
function getHeaderPairs(headers, kind) { |
|
const keys = Object.keys(headers[MAP]).sort(); |
|
return keys.map(kind === 'key' ? function (k) { |
|
return [k]; |
|
} : function (k) { |
|
return [k, headers.get(k)]; |
|
}); |
|
} |
|
|
|
const INTERNAL = Symbol('internal'); |
|
|
|
function createHeadersIterator(target, kind) { |
|
const iterator = Object.create(HeadersIteratorPrototype); |
|
iterator[INTERNAL] = { |
|
target, |
|
kind, |
|
index: 0 |
|
}; |
|
return iterator; |
|
} |
|
|
|
const HeadersIteratorPrototype = Object.setPrototypeOf({ |
|
next() { |
|
// istanbul ignore if |
|
if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { |
|
throw new TypeError('Value of `this` is not a HeadersIterator'); |
|
} |
|
|
|
var _INTERNAL = this[INTERNAL]; |
|
const target = _INTERNAL.target, |
|
kind = _INTERNAL.kind, |
|
index = _INTERNAL.index; |
|
|
|
const values = getHeaderPairs(target, kind); |
|
const len = values.length; |
|
if (index >= len) { |
|
return { |
|
value: undefined, |
|
done: true |
|
}; |
|
} |
|
|
|
const pair = values[index]; |
|
this[INTERNAL].index = index + 1; |
|
|
|
let result; |
|
if (kind === 'key') { |
|
result = pair[0]; |
|
} else if (kind === 'value') { |
|
result = pair[1]; |
|
} else { |
|
result = pair; |
|
} |
|
|
|
return { |
|
value: result, |
|
done: false |
|
}; |
|
} |
|
}, Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))); |
|
|
|
Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { |
|
value: 'HeadersIterator', |
|
writable: false, |
|
enumerable: false, |
|
configurable: true |
|
}); |
|
|
|
/** |
|
* response.js |
|
* |
|
* Response class provides content decoding |
|
*/ |
|
|
|
var _require$2 = require('http'); |
|
|
|
const STATUS_CODES = _require$2.STATUS_CODES; |
|
|
|
/** |
|
* Response class |
|
* |
|
* @param Stream body Readable stream |
|
* @param Object opts Response options |
|
* @return Void |
|
*/ |
|
|
|
class Response { |
|
constructor() { |
|
let body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; |
|
let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; |
|
|
|
Body.call(this, body, opts); |
|
|
|
this.url = opts.url; |
|
this.status = opts.status || 200; |
|
this.statusText = opts.statusText || STATUS_CODES[this.status]; |
|
|
|
this.headers = new Headers(opts.headers); |
|
|
|
Object.defineProperty(this, Symbol.toStringTag, { |
|
value: 'Response', |
|
writable: false, |
|
enumerable: false, |
|
configurable: true |
|
}); |
|
} |
|
|
|
/** |
|
* Convenience property representing if the request ended normally |
|
*/ |
|
get ok() { |
|
return this.status >= 200 && this.status < 300; |
|
} |
|
|
|
/** |
|
* Clone this response |
|
* |
|
* @return Response |
|
*/ |
|
clone() { |
|
|
|
return new Response(clone(this), { |
|
url: this.url, |
|
status: this.status, |
|
statusText: this.statusText, |
|
headers: this.headers, |
|
ok: this.ok |
|
}); |
|
} |
|
} |
|
|
|
Body.mixIn(Response.prototype); |
|
|
|
Object.defineProperty(Response.prototype, Symbol.toStringTag, { |
|
value: 'ResponsePrototype', |
|
writable: false, |
|
enumerable: false, |
|
configurable: true |
|
}); |
|
|
|
/** |
|
* request.js |
|
* |
|
* Request class contains server only options |
|
*/ |
|
|
|
var _require$3 = require('url'); |
|
|
|
const format_url = _require$3.format; |
|
const parse_url = _require$3.parse; |
|
|
|
|
|
const PARSED_URL = Symbol('url'); |
|
|
|
/** |
|
* Request class |
|
* |
|
* @param Mixed input Url or Request instance |
|
* @param Object init Custom options |
|
* @return Void |
|
*/ |
|
class Request { |
|
constructor(input) { |
|
let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; |
|
|
|
let parsedURL; |
|
|
|
// normalize input |
|
if (!(input instanceof Request)) { |
|
if (input && input.href) { |
|
// in order to support Node.js' Url objects; though WHATWG's URL objects |
|
// will fall into this branch also (since their `toString()` will return |
|
// `href` property anyway) |
|
parsedURL = parse_url(input.href); |
|
} else { |
|
// coerce input to a string before attempting to parse |
|
parsedURL = parse_url(`${input}`); |
|
} |
|
input = {}; |
|
} else { |
|
parsedURL = parse_url(input.url); |
|
} |
|
|
|
let method = init.method || input.method || 'GET'; |
|
|
|
if ((init.body != null || input instanceof Request && input.body !== null) && (method === 'GET' || method === 'HEAD')) { |
|
throw new TypeError('Request with GET/HEAD method cannot have body'); |
|
} |
|
|
|
let inputBody = init.body != null ? init.body : input instanceof Request && input.body !== null ? clone(input) : null; |
|
|
|
Body.call(this, inputBody, { |
|
timeout: init.timeout || input.timeout || 0, |
|
size: init.size || input.size || 0 |
|
}); |
|
|
|
// fetch spec options |
|
this.method = method.toUpperCase(); |
|
this.redirect = init.redirect || input.redirect || 'follow'; |
|
this.headers = new Headers(init.headers || input.headers || {}); |
|
|
|
if (init.body != null) { |
|
const contentType = extractContentType(this); |
|
if (contentType !== null && !this.headers.has('Content-Type')) { |
|
this.headers.append('Content-Type', contentType); |
|
} |
|
} |
|
|
|
// server only options |
|
this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; |
|
this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; |
|
this.counter = init.counter || input.counter || 0; |
|
this.agent = init.agent || input.agent; |
|
|
|
this[PARSED_URL] = parsedURL; |
|
Object.defineProperty(this, Symbol.toStringTag, { |
|
value: 'Request', |
|
writable: false, |
|
enumerable: false, |
|
configurable: true |
|
}); |
|
} |
|
|
|
get url() { |
|
return format_url(this[PARSED_URL]); |
|
} |
|
|
|
/** |
|
* Clone this request |
|
* |
|
* @return Request |
|
*/ |
|
clone() { |
|
return new Request(this); |
|
} |
|
} |
|
|
|
Body.mixIn(Request.prototype); |
|
|
|
Object.defineProperty(Request.prototype, Symbol.toStringTag, { |
|
value: 'RequestPrototype', |
|
writable: false, |
|
enumerable: false, |
|
configurable: true |
|
}); |
|
|
|
function getNodeRequestOptions(request) { |
|
const parsedURL = request[PARSED_URL]; |
|
const headers = new Headers(request.headers); |
|
|
|
// fetch step 3 |
|
if (!headers.has('Accept')) { |
|
headers.set('Accept', '*/*'); |
|
} |
|
|
|
// Basic fetch |
|
if (!parsedURL.protocol || !parsedURL.hostname) { |
|
throw new TypeError('Only absolute URLs are supported'); |
|
} |
|
|
|
if (!/^https?:$/.test(parsedURL.protocol)) { |
|
throw new TypeError('Only HTTP(S) protocols are supported'); |
|
} |
|
|
|
// HTTP-network-or-cache fetch steps 5-9 |
|
let contentLengthValue = null; |
|
if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { |
|
contentLengthValue = '0'; |
|
} |
|
if (request.body != null) { |
|
const totalBytes = getTotalBytes(request); |
|
if (typeof totalBytes === 'number') { |
|
contentLengthValue = String(totalBytes); |
|
} |
|
} |
|
if (contentLengthValue) { |
|
headers.set('Content-Length', contentLengthValue); |
|
} |
|
|
|
// HTTP-network-or-cache fetch step 12 |
|
if (!headers.has('User-Agent')) { |
|
headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); |
|
} |
|
|
|
// HTTP-network-or-cache fetch step 16 |
|
if (request.compress) { |
|
headers.set('Accept-Encoding', 'gzip,deflate'); |
|
} |
|
if (!headers.has('Connection') && !request.agent) { |
|
headers.set('Connection', 'close'); |
|
} |
|
|
|
// HTTP-network fetch step 4 |
|
// chunked encoding is handled by Node.js |
|
|
|
return Object.assign({}, parsedURL, { |
|
method: request.method, |
|
headers: headers.raw(), |
|
agent: request.agent |
|
}); |
|
} |
|
|
|
/** |
|
* index.js |
|
* |
|
* a request API compatible with window.fetch |
|
*/ |
|
|
|
const http = require('http'); |
|
const https = require('https'); |
|
|
|
var _require = require('stream'); |
|
|
|
const PassThrough = _require.PassThrough; |
|
|
|
var _require2 = require('url'); |
|
|
|
const resolve_url = _require2.resolve; |
|
|
|
const zlib = require('zlib'); |
|
|
|
/** |
|
* Fetch function |
|
* |
|
* @param Mixed url Absolute url or Request instance |
|
* @param Object opts Fetch options |
|
* @return Promise |
|
*/ |
|
function 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; |
|
|
|
// wrap http.request into fetch |
|
return new fetch.Promise(function (resolve, reject) { |
|
// build request object |
|
const request = new Request(url, opts); |
|
const options = getNodeRequestOptions(request); |
|
|
|
const send = (options.protocol === 'https:' ? https : http).request; |
|
|
|
// 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 |
|
const req = send(options); |
|
let reqTimeout; |
|
|
|
if (request.timeout) { |
|
req.once('socket', function (socket) { |
|
reqTimeout = setTimeout(function () { |
|
req.abort(); |
|
reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); |
|
}, request.timeout); |
|
}); |
|
} |
|
|
|
req.on('error', function (err) { |
|
clearTimeout(reqTimeout); |
|
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); |
|
}); |
|
|
|
req.on('response', function (res) { |
|
clearTimeout(reqTimeout); |
|
|
|
// handle redirect |
|
if (fetch.isRedirect(res.statusCode) && request.redirect !== 'manual') { |
|
if (request.redirect === 'error') { |
|
reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect')); |
|
return; |
|
} |
|
|
|
if (request.counter >= request.follow) { |
|
reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect')); |
|
return; |
|
} |
|
|
|
if (!res.headers.location) { |
|
reject(new FetchError(`redirect location header missing at: ${request.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) && request.method === 'POST') { |
|
request.method = 'GET'; |
|
request.body = null; |
|
request.headers.delete('content-length'); |
|
} |
|
|
|
request.counter++; |
|
|
|
resolve(fetch(resolve_url(request.url, res.headers.location), request)); |
|
return; |
|
} |
|
|
|
// normalize location header for manual redirect mode |
|
const headers = new Headers(); |
|
for (const name of Object.keys(res.headers)) { |
|
if (Array.isArray(res.headers[name])) { |
|
for (const val of res.headers[name]) { |
|
headers.append(name, val); |
|
} |
|
} else { |
|
headers.append(name, res.headers[name]); |
|
} |
|
} |
|
if (request.redirect === 'manual' && headers.has('location')) { |
|
headers.set('location', resolve_url(request.url, headers.get('location'))); |
|
} |
|
|
|
// prepare response |
|
let body = res.pipe(new PassThrough()); |
|
const response_options = { |
|
url: request.url, |
|
status: res.statusCode, |
|
statusText: res.statusMessage, |
|
headers: headers, |
|
size: request.size, |
|
timeout: request.timeout |
|
}; |
|
|
|
// HTTP-network fetch step 16.1.2 |
|
const codings = headers.get('Content-Encoding'); |
|
|
|
// HTTP-network fetch step 16.1.3: handle content codings |
|
|
|
// 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 (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { |
|
resolve(new Response(body, response_options)); |
|
return; |
|
} |
|
|
|
// For Node v6+ |
|
// Be less strict when decoding compressed responses, since sometimes |
|
// servers send slightly invalid responses that are still accepted |
|
// by common browsers. |
|
// Always using Z_SYNC_FLUSH is what cURL does. |
|
const zlibOptions = { |
|
flush: zlib.Z_SYNC_FLUSH, |
|
finishFlush: zlib.Z_SYNC_FLUSH |
|
}; |
|
|
|
// for gzip |
|
if (codings == 'gzip' || codings == 'x-gzip') { |
|
body = body.pipe(zlib.createGunzip(zlibOptions)); |
|
resolve(new Response(body, response_options)); |
|
return; |
|
} |
|
|
|
// for deflate |
|
if (codings == 'deflate' || codings == 'x-deflate') { |
|
// handle the infamous raw deflate response from old servers |
|
// a hack for old IIS and Apache servers |
|
const raw = res.pipe(new 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()); |
|
} |
|
resolve(new Response(body, response_options)); |
|
}); |
|
return; |
|
} |
|
|
|
// otherwise, use response as-is |
|
resolve(new Response(body, response_options)); |
|
}); |
|
|
|
writeToStream(req, request); |
|
}); |
|
} |
|
|
|
/** |
|
* Redirect code matching |
|
* |
|
* @param Number code Status code |
|
* @return Boolean |
|
*/ |
|
fetch.isRedirect = function (code) { |
|
return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; |
|
}; |
|
|
|
// expose Promise |
|
fetch.Promise = global.Promise; |
|
|
|
module.exports = exports = fetch; |
|
exports.Headers = Headers; |
|
exports.Request = Request; |
|
exports.Response = Response; |
|
exports.FetchError = FetchError;
|
|
|