NeahNew/node_modules/openid-client/lib/helpers/request.js
2025-05-03 14:17:46 +02:00

201 lines
5.0 KiB
JavaScript

const assert = require('assert');
const querystring = require('querystring');
const http = require('http');
const https = require('https');
const { once } = require('events');
const { URL } = require('url');
const LRU = require('lru-cache');
const pkg = require('../../package.json');
const { RPError } = require('../errors');
const pick = require('./pick');
const { deep: defaultsDeep } = require('./defaults');
const { HTTP_OPTIONS } = require('./consts');
let DEFAULT_HTTP_OPTIONS;
const NQCHAR = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
const allowed = [
'agent',
'ca',
'cert',
'crl',
'headers',
'key',
'lookup',
'passphrase',
'pfx',
'timeout',
];
const setDefaults = (props, options) => {
DEFAULT_HTTP_OPTIONS = defaultsDeep(
{},
props.length ? pick(options, ...props) : options,
DEFAULT_HTTP_OPTIONS,
);
};
setDefaults([], {
headers: {
'User-Agent': `${pkg.name}/${pkg.version} (${pkg.homepage})`,
'Accept-Encoding': 'identity',
},
timeout: 3500,
});
function send(req, body, contentType) {
if (contentType) {
req.removeHeader('content-type');
req.setHeader('content-type', contentType);
}
if (body) {
req.removeHeader('content-length');
req.setHeader('content-length', Buffer.byteLength(body));
req.write(body);
}
req.end();
}
const nonces = new LRU({ max: 100 });
module.exports = async function request(options, { accessToken, mTLS = false, DPoP } = {}) {
let url;
try {
url = new URL(options.url);
delete options.url;
assert(/^(https?:)$/.test(url.protocol));
} catch (err) {
throw new TypeError('only valid absolute URLs can be requested');
}
const optsFn = this[HTTP_OPTIONS];
let opts = options;
const nonceKey = `${url.origin}${url.pathname}`;
if (DPoP && 'dpopProof' in this) {
opts.headers = opts.headers || {};
opts.headers.DPoP = await this.dpopProof(
{
htu: `${url.origin}${url.pathname}`,
htm: options.method || 'GET',
nonce: nonces.get(nonceKey),
},
DPoP,
accessToken,
);
}
let userOptions;
if (optsFn) {
userOptions = pick(
optsFn.call(this, url, defaultsDeep({}, opts, DEFAULT_HTTP_OPTIONS)),
...allowed,
);
}
opts = defaultsDeep({}, userOptions, opts, DEFAULT_HTTP_OPTIONS);
if (mTLS && !opts.pfx && !(opts.key && opts.cert)) {
throw new TypeError('mutual-TLS certificate and key not set');
}
if (opts.searchParams) {
for (const [key, value] of Object.entries(opts.searchParams)) {
url.searchParams.delete(key);
url.searchParams.set(key, value);
}
}
let responseType;
let form;
let json;
let body;
({ form, responseType, json, body, ...opts } = opts);
for (const [key, value] of Object.entries(opts.headers || {})) {
if (value === undefined) {
delete opts.headers[key];
}
}
let response;
const req = (url.protocol === 'https:' ? https.request : http.request)(url.href, opts);
return (async () => {
if (json) {
send(req, JSON.stringify(json), 'application/json');
} else if (form) {
send(req, querystring.stringify(form), 'application/x-www-form-urlencoded');
} else if (body) {
send(req, body);
} else {
send(req);
}
[response] = await Promise.race([once(req, 'response'), once(req, 'timeout')]);
// timeout reached
if (!response) {
req.destroy();
throw new RPError(`outgoing request timed out after ${opts.timeout}ms`);
}
const parts = [];
for await (const part of response) {
parts.push(part);
}
if (parts.length) {
switch (responseType) {
case 'json': {
Object.defineProperty(response, 'body', {
get() {
let value = Buffer.concat(parts);
try {
value = JSON.parse(value);
} catch (err) {
Object.defineProperty(err, 'response', { value: response });
throw err;
} finally {
Object.defineProperty(response, 'body', { value, configurable: true });
}
return value;
},
configurable: true,
});
break;
}
case undefined:
case 'buffer': {
Object.defineProperty(response, 'body', {
get() {
const value = Buffer.concat(parts);
Object.defineProperty(response, 'body', { value, configurable: true });
return value;
},
configurable: true,
});
break;
}
default:
throw new TypeError('unsupported responseType request option');
}
}
return response;
})()
.catch((err) => {
if (response) Object.defineProperty(err, 'response', { value: response });
throw err;
})
.finally(() => {
const dpopNonce = response && response.headers['dpop-nonce'];
if (dpopNonce && NQCHAR.test(dpopNonce)) {
nonces.set(nonceKey, dpopNonce);
}
});
};
module.exports.setDefaults = setDefaults.bind(undefined, allowed);