UNPKG

@sap/xssec

Version:

XS Advanced Container Security API for node.js

188 lines (160 loc) 5.33 kB
const https = require('https'); const http = require('http'); const zlib = require("zlib"); const url = require('url'); /** * Select the appropriate request module based on protocol * @param {string} protocol * @returns {module} */ function selectRequestModule(protocol) { switch (protocol) { case 'https:': return https; case 'http:': return http; default: throw new Error(`Unsupported protocol: ${protocol}`); } } class FetchError extends Error { constructor(message, error) { super(message); Error.captureStackTrace(this, this.constructor); this.code = this.errno = error?.code; this.erroredSysCall = error?.syscall; } get name() { return this.constructor.name; } } class Response { #response; #request; constructor(response, request) { this.#response = response; this.#request = request; } async #getZip() { const response = this.#response; return new Promise((resolve, reject) => { const gunzip = zlib.createGunzip(); response.pipe(gunzip); const data = []; gunzip.on('data', function (chunk) { data.push(chunk.toString()); }); gunzip.on('end', function () { resolve(data.join('')); }); gunzip.on('error', (error) => { reject(new FetchError(`request to ${this.requestUrl} failed, reason: ${error.message}`, error)); }); }); } get requestUrl() { const req = this.#request; return req.protocol + "//" + req.host + req.path; } async #getText() { const response = this.#response; if (response.headers['content-encoding'] === 'gzip') { return this.#getZip(); } return new Promise((resolve, reject) => { const data = []; response.setEncoding('utf8'); response.on('data', function (chunk) { data.push(chunk); }); response.on('end', function () { resolve(data.join('')); }); response.on('error', (error) => { reject(new FetchError(`request to ${this.requestUrl} failed, reason: ${error.message}`, error)); }); }); } async json() { return JSON.parse(await this.#getText()); } async text() { return this.#getText(); } get ok() { //https://developer.mozilla.org/en-US/docs/Web/API/Response/ok return this.status >= 200 && this.status < 300; } get status() { return this.#response.statusCode; } get headers() { //simply create a map from the headers. const headers = new Map(); for (const [key, value] of Object.entries(this.#response.headers)) { headers.set(key, value); } return headers; } } /** * A simple fetch implementation with basic functionality using node's https module. * This implementation has the same API as node-fetch but with limited functionality. * @param {string|URL} inputUrl * @param {https.RequestOptions} options * @returns {Response} * @throws {FetchError} */ async function xssec_fetch(inputUrl, options = {}) { importDefaultOptions(options); importDefaultHeaders(options); importBodyOptions(options); const requestModule = selectRequestModule(new url.URL(inputUrl).protocol); return new Promise(function (resolve, reject) { const req = requestModule.request(inputUrl, options, (response) => { resolve(new Response(response, req)); }); req.on('error', (error) => { reject(new FetchError(`request to ${inputUrl} failed, reason: ${error.message}`, error)); }); req.on('timeout', () => { req.destroy(); reject(new FetchError(`request to ${inputUrl} timed out.`, { code: 'ETIMEDOUT' })); }); if (options.data) { req.write(options.data); } req.end(); }); } function importDefaultOptions(options) { options.method ??= 'GET'; } function importBodyOptions(options) { if (options.body) { const method = options.method.toUpperCase(); if (method !== 'GET' && method !== 'HEAD') { if (options.json) { options.data = JSON.stringify(options.body); options.headers['Content-Type'] = 'application/json;charset=UTF-8'; } else { options.data = options.body.toString(); options.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; } options.headers['Content-Length'] = Buffer.byteLength(options.data); delete options.body; } else { throw new Error("Request with GET/HEAD method cannot have body"); } } } function importDefaultHeaders(options) { if (options.headers == null) { options.headers = {}; } options.headers['Accept-Encoding'] = 'gzip,deflate'; if (!options.headers['Accept'] && !options.headers['accept']) { options.headers['Accept'] = '*/*'; } } module.exports = xssec_fetch;