@sap/xssec
Version:
XS Advanced Container Security API for node.js
188 lines (160 loc) • 5.33 kB
JavaScript
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;