mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Fixes #3082 GOT will throw errors for non-successful http responses by default. We need to turn that off to be consistent with the 1.x behaviour using the request module
581 lines
25 KiB
JavaScript
581 lines
25 KiB
JavaScript
/**
|
|
* Copyright JS Foundation and other contributors, http://js.foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
**/
|
|
|
|
module.exports = function(RED) {
|
|
"use strict";
|
|
const got = require("got");
|
|
const {CookieJar} = require("tough-cookie");
|
|
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
|
|
const FormData = require('form-data');
|
|
const { v4: uuid } = require('uuid');
|
|
const crypto = require('crypto');
|
|
const URL = require("url").URL
|
|
var mustache = require("mustache");
|
|
var querystring = require("querystring");
|
|
var cookie = require("cookie");
|
|
var hashSum = require("hash-sum");
|
|
|
|
|
|
// Cache a reference to the existing https.request function
|
|
// so we can compare later to see if an old agent-base instance
|
|
// has been required.
|
|
// This is generally okay as the core nodes are required before
|
|
// any contrib nodes. Where it will fail is if the agent-base module
|
|
// is required via the settings file or outside of Node-RED before it
|
|
// is started.
|
|
// If there are other modules that patch the function, they will get undone
|
|
// as well. Not much we can do about that right now. Patching core
|
|
// functions is bad.
|
|
const HTTPS_MODULE = require("https");
|
|
const HTTPS_REQUEST = HTTPS_MODULE.request;
|
|
|
|
function checkNodeAgentPatch() {
|
|
if (HTTPS_MODULE.request !== HTTPS_REQUEST && HTTPS_MODULE.request.length === 2) {
|
|
RED.log.warn(`
|
|
|
|
---------------------------------------------------------------------
|
|
Patched https.request function detected. This will break the
|
|
HTTP Request node. The original code has now been restored.
|
|
|
|
This is likely caused by a contrib node including an old version of
|
|
the 'agent-base@<5.0.0' module.
|
|
|
|
You can identify what node is at fault by running:
|
|
npm list agent-base
|
|
in your Node-RED user directory (${RED.settings.userDir}).
|
|
---------------------------------------------------------------------
|
|
`);
|
|
HTTPS_MODULE.request = HTTPS_REQUEST
|
|
}
|
|
}
|
|
|
|
function HTTPRequest(n) {
|
|
RED.nodes.createNode(this,n);
|
|
checkNodeAgentPatch();
|
|
var node = this;
|
|
var nodeUrl = n.url;
|
|
var isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1;
|
|
var nodeMethod = n.method || "GET";
|
|
var paytoqs = false;
|
|
var paytobody = false;
|
|
var redirectList = [];
|
|
|
|
var nodeHTTPPersistent = n["persist"];
|
|
if (n.tls) {
|
|
var tlsNode = RED.nodes.getNode(n.tls);
|
|
}
|
|
this.ret = n.ret || "txt";
|
|
this.authType = n.authType || "basic";
|
|
if (RED.settings.httpRequestTimeout) { this.reqTimeout = parseInt(RED.settings.httpRequestTimeout) || 120000; }
|
|
else { this.reqTimeout = 120000; }
|
|
|
|
if (n.paytoqs === true || n.paytoqs === "query") { paytoqs = true; }
|
|
else if (n.paytoqs === "body") { paytobody = true; }
|
|
|
|
|
|
var prox, noprox;
|
|
if (process.env.http_proxy) { prox = process.env.http_proxy; }
|
|
if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; }
|
|
if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); }
|
|
if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); }
|
|
|
|
var proxyConfig = null;
|
|
if (n.proxy) {
|
|
proxyConfig = RED.nodes.getNode(n.proxy);
|
|
prox = proxyConfig.url;
|
|
noprox = proxyConfig.noproxy;
|
|
}
|
|
|
|
this.on("input",function(msg,nodeSend,nodeDone) {
|
|
checkNodeAgentPatch();
|
|
//reset redirectList on each request
|
|
redirectList = [];
|
|
var preRequestTimestamp = process.hrtime();
|
|
node.status({fill:"blue",shape:"dot",text:"httpin.status.requesting"});
|
|
var url = nodeUrl || msg.url;
|
|
if (msg.url && nodeUrl && (nodeUrl !== msg.url)) { // revert change below when warning is finally removed
|
|
node.warn(RED._("common.errors.nooverride"));
|
|
}
|
|
|
|
if (isTemplatedUrl) {
|
|
url = mustache.render(nodeUrl,msg);
|
|
}
|
|
if (!url) {
|
|
node.error(RED._("httpin.errors.no-url"),msg);
|
|
nodeDone();
|
|
return;
|
|
}
|
|
|
|
|
|
// url must start http:// or https:// so assume http:// if not set
|
|
if (url.indexOf("://") !== -1 && url.indexOf("http") !== 0) {
|
|
node.warn(RED._("httpin.errors.invalid-transport"));
|
|
node.status({fill:"red",shape:"ring",text:"httpin.errors.invalid-transport"});
|
|
nodeDone();
|
|
return;
|
|
}
|
|
if (!((url.indexOf("http://") === 0) || (url.indexOf("https://") === 0))) {
|
|
if (tlsNode) {
|
|
url = "https://"+url;
|
|
} else {
|
|
url = "http://"+url;
|
|
}
|
|
}
|
|
url = encodeURI(url);
|
|
|
|
var method = nodeMethod.toUpperCase() || "GET";
|
|
if (msg.method && n.method && (n.method !== "use")) { // warn if override option not set
|
|
node.warn(RED._("common.errors.nooverride"));
|
|
}
|
|
if (msg.method && n.method && (n.method === "use")) {
|
|
method = msg.method.toUpperCase(); // use the msg parameter
|
|
}
|
|
|
|
// var isHttps = (/^https/i.test(url));
|
|
|
|
var opts = {};
|
|
// set defaultport, else when using HttpsProxyAgent, it's defaultPort of 443 will be used :(.
|
|
// Had to remove this to get http->https redirect to work
|
|
// opts.defaultPort = isHttps?443:80;
|
|
opts.timeout = node.reqTimeout;
|
|
opts.throwHttpErrors = false;
|
|
opts.method = method;
|
|
opts.headers = {};
|
|
opts.retry = 0;
|
|
opts.responseType = 'buffer';
|
|
opts.maxRedirects = 21;
|
|
opts.cookieJar = new CookieJar();
|
|
opts.ignoreInvalidCookies = true;
|
|
opts.forever = nodeHTTPPersistent;
|
|
if (msg.requestTimeout !== undefined) {
|
|
if (isNaN(msg.requestTimeout)) {
|
|
node.warn(RED._("httpin.errors.timeout-isnan"));
|
|
} else if (msg.requestTimeout < 1) {
|
|
node.warn(RED._("httpin.errors.timeout-isnegative"));
|
|
} else {
|
|
opts.timeout = msg.requestTimeout;
|
|
}
|
|
}
|
|
|
|
opts.hooks = {
|
|
beforeRedirect: [
|
|
(options, response) => {
|
|
let redirectInfo = {
|
|
location: response.headers.location
|
|
}
|
|
if (response.headers.hasOwnProperty('set-cookie')) {
|
|
redirectInfo.cookies = extractCookies(response.headers['set-cookie']);
|
|
}
|
|
redirectList.push(redirectInfo)
|
|
}
|
|
]
|
|
}
|
|
|
|
var ctSet = "Content-Type"; // set default camel case
|
|
var clSet = "Content-Length";
|
|
if (msg.headers) {
|
|
if (msg.headers.hasOwnProperty('x-node-red-request-node')) {
|
|
var headerHash = msg.headers['x-node-red-request-node'];
|
|
delete msg.headers['x-node-red-request-node'];
|
|
var hash = hashSum(msg.headers);
|
|
if (hash === headerHash) {
|
|
delete msg.headers;
|
|
}
|
|
}
|
|
if (msg.headers) {
|
|
for (var v in msg.headers) {
|
|
if (msg.headers.hasOwnProperty(v)) {
|
|
var name = v.toLowerCase();
|
|
if (name !== "content-type" && name !== "content-length") {
|
|
// only normalise the known headers used later in this
|
|
// function. Otherwise leave them alone.
|
|
name = v;
|
|
}
|
|
else if (name === 'content-type') { ctSet = v; }
|
|
else { clSet = v; }
|
|
opts.headers[name] = msg.headers[v];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (msg.hasOwnProperty('followRedirects')) {
|
|
opts.followRedirect = !!msg.followRedirects;
|
|
}
|
|
|
|
if (opts.headers.hasOwnProperty('cookie')) {
|
|
var cookies = cookie.parse(opts.headers.cookie, {decode:String});
|
|
for (var name in cookies) {
|
|
opts.cookieJar.setCookie(cookie.serialize(name, cookies[name], {encode:String}), url, {ignoreError: true});
|
|
}
|
|
delete opts.headers.cookie;
|
|
}
|
|
if (msg.cookies) {
|
|
for (var name in msg.cookies) {
|
|
if (msg.cookies.hasOwnProperty(name)) {
|
|
if (msg.cookies[name] === null || msg.cookies[name].value === null) {
|
|
// This case clears a cookie for HTTP In/Response nodes.
|
|
// Ignore for this node.
|
|
} else if (typeof msg.cookies[name] === 'object') {
|
|
if(msg.cookies[name].encode === false){
|
|
// If the encode option is false, the value is not encoded.
|
|
opts.cookieJar.setCookie(cookie.serialize(name, msg.cookies[name].value, {encode: String}), url, {ignoreError: true});
|
|
} else {
|
|
// The value is encoded by encodeURIComponent().
|
|
opts.cookieJar.setCookie(cookie.serialize(name, msg.cookies[name].value), url, {ignoreError: true});
|
|
}
|
|
} else {
|
|
opts.cookieJar.setCookie(cookie.serialize(name, msg.cookies[name]), url, {ignoreError: true});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var parsedURL = new URL(url)
|
|
this.credentials = this.credentials || {}
|
|
if (parsedURL.username && !this.credentials.user) {
|
|
this.credentials.user = parsedURL.username
|
|
}
|
|
if (parsedURL.password && !this.credentials.password) {
|
|
this.credentials.password = parsedURL.password
|
|
}
|
|
if (Object.keys(this.credentials).length != 0) {
|
|
if (this.authType === "basic") {
|
|
// Workaround for https://github.com/sindresorhus/got/issues/1169
|
|
var cred = ""
|
|
if (this.credentials.user) {
|
|
// opts.username = this.credentials.user;
|
|
cred = this.credentials.user
|
|
}
|
|
if (this.credentials.password) {
|
|
// opts.password = this.credentials.password;
|
|
cred += ":" + this.credentials.password
|
|
}
|
|
// build own basic auth header
|
|
opts.headers.Authorization = "Basic " + Buffer.from(cred).toString("base64");
|
|
} else if (this.authType === "digest") {
|
|
let digestCreds = this.credentials;
|
|
let sentCreds = false;
|
|
opts.hooks.afterResponse = [(response, retry) => {
|
|
if (response.statusCode === 401) {
|
|
if (sentCreds) {
|
|
return response
|
|
}
|
|
const requestUrl = new URL(response.request.requestUrl);
|
|
const options = response.request.options;
|
|
const normalisedHeaders = {};
|
|
Object.keys(response.headers).forEach(k => {
|
|
normalisedHeaders[k.toLowerCase()] = response.headers[k]
|
|
})
|
|
if (normalisedHeaders['www-authenticate']) {
|
|
let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, options.method, requestUrl.pathname, normalisedHeaders['www-authenticate'])
|
|
options.headers.authorization = authHeader;
|
|
}
|
|
sentCreds = true;
|
|
return retry(options);
|
|
}
|
|
return response
|
|
}];
|
|
} else if (this.authType === "bearer") {
|
|
opts.headers.Authorization = `Bearer ${this.credentials.password||""}`
|
|
}
|
|
}
|
|
var payload = null;
|
|
|
|
|
|
if (method !== 'GET' && method !== 'HEAD' && typeof msg.payload !== "undefined") {
|
|
if (opts.headers['content-type'] == 'multipart/form-data' && typeof msg.payload === "object") {
|
|
let formData = new FormData();
|
|
for (var opt in msg.payload) {
|
|
if (msg.payload.hasOwnProperty(opt)) {
|
|
var val = msg.payload[opt];
|
|
if (val !== undefined && val !== null) {
|
|
if (typeof val === 'string' || Buffer.isBuffer(val)) {
|
|
formData.append(opt, val);
|
|
} else if (typeof val === 'object' && val.hasOwnProperty('value')) {
|
|
formData.append(opt,val.value,val.options || {});
|
|
} else {
|
|
formData.append(opt,JSON.stringify(val));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// GOT will only set the content-type header with the correct boundary
|
|
// if the header isn't set. So we delete it here, for GOT to reset it.
|
|
delete opts.headers['content-type'];
|
|
opts.body = formData;
|
|
} else {
|
|
if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) {
|
|
payload = msg.payload;
|
|
} else if (typeof msg.payload == "number") {
|
|
payload = msg.payload+"";
|
|
} else {
|
|
if (opts.headers['content-type'] == 'application/x-www-form-urlencoded') {
|
|
payload = querystring.stringify(msg.payload);
|
|
} else {
|
|
payload = JSON.stringify(msg.payload);
|
|
if (opts.headers['content-type'] == null) {
|
|
opts.headers[ctSet] = "application/json";
|
|
}
|
|
}
|
|
}
|
|
if (opts.headers['content-length'] == null) {
|
|
if (Buffer.isBuffer(payload)) {
|
|
opts.headers[clSet] = payload.length;
|
|
} else {
|
|
opts.headers[clSet] = Buffer.byteLength(payload);
|
|
}
|
|
}
|
|
opts.body = payload;
|
|
}
|
|
}
|
|
|
|
|
|
if (method == 'GET' && typeof msg.payload !== "undefined" && paytoqs) {
|
|
if (typeof msg.payload === "object") {
|
|
try {
|
|
if (url.indexOf("?") !== -1) {
|
|
url += (url.endsWith("?")?"":"&") + querystring.stringify(msg.payload);
|
|
} else {
|
|
url += "?" + querystring.stringify(msg.payload);
|
|
}
|
|
} catch(err) {
|
|
|
|
node.error(RED._("httpin.errors.invalid-payload"),msg);
|
|
nodeDone();
|
|
return;
|
|
}
|
|
} else {
|
|
|
|
node.error(RED._("httpin.errors.invalid-payload"),msg);
|
|
nodeDone();
|
|
return;
|
|
}
|
|
} else if ( method == "GET" && typeof msg.payload !== "undefined" && paytobody) {
|
|
opts.allowGetBody = true;
|
|
if (typeof msg.payload === "object") {
|
|
opts.body = JSON.stringify(msg.payload);
|
|
} else if (typeof msg.payload == "number") {
|
|
opts.body = msg.payload+"";
|
|
} else if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) {
|
|
opts.body = msg.payload;
|
|
}
|
|
}
|
|
|
|
// revert to user supplied Capitalisation if needed.
|
|
if (opts.headers.hasOwnProperty('content-type') && (ctSet !== 'content-type')) {
|
|
opts.headers[ctSet] = opts.headers['content-type'];
|
|
delete opts.headers['content-type'];
|
|
}
|
|
if (opts.headers.hasOwnProperty('content-length') && (clSet !== 'content-length')) {
|
|
opts.headers[clSet] = opts.headers['content-length'];
|
|
delete opts.headers['content-length'];
|
|
}
|
|
|
|
var noproxy;
|
|
if (noprox) {
|
|
for (var i = 0; i < noprox.length; i += 1) {
|
|
if (url.indexOf(noprox[i]) !== -1) { noproxy=true; }
|
|
}
|
|
}
|
|
if (prox && !noproxy) {
|
|
var match = prox.match(/^(https?:\/\/)?(.+)?:([0-9]+)?/i);
|
|
if (match) {
|
|
let proxyAgent;
|
|
let proxyURL = new URL(prox);
|
|
//set username/password to null to stop empty creds header
|
|
let proxyOptions = {
|
|
proxy: {
|
|
protocol: proxyURL.protocol,
|
|
hostname: proxyURL.hostname,
|
|
port: proxyURL.port,
|
|
username: null,
|
|
password: null
|
|
},
|
|
maxFreeSockets: 256,
|
|
maxSockets: 256,
|
|
keepAlive: true
|
|
}
|
|
if (proxyConfig && proxyConfig.credentials) {
|
|
let proxyUsername = proxyConfig.credentials.username || '';
|
|
let proxyPassword = proxyConfig.credentials.password || '';
|
|
if (proxyUsername || proxyPassword) {
|
|
proxyOptions.proxy.username = proxyUsername;
|
|
proxyOptions.proxy.password = proxyPassword;
|
|
}
|
|
} else if (proxyURL.username || proxyURL.password){
|
|
proxyOptions.proxy.username = proxyURL.username;
|
|
proxyOptions.proxy.password = proxyURL.password;
|
|
}
|
|
//need both incase of http -> https redirect
|
|
opts.agent = {
|
|
http: new HttpProxyAgent(proxyOptions),
|
|
https: new HttpsProxyAgent(proxyOptions)
|
|
};
|
|
|
|
} else {
|
|
node.warn("Bad proxy url: "+ prox);
|
|
}
|
|
}
|
|
if (tlsNode) {
|
|
opts.https = {};
|
|
tlsNode.addTLSOptions(opts.https);
|
|
} else {
|
|
if (msg.hasOwnProperty('rejectUnauthorized')) {
|
|
opts.https = { rejectUnauthorized: msg.rejectUnauthorized };
|
|
}
|
|
}
|
|
got(url,opts).then(res => {
|
|
msg.statusCode = res.statusCode;
|
|
msg.headers = res.headers;
|
|
msg.responseUrl = res.url;
|
|
msg.payload = res.body;
|
|
msg.redirectList = redirectList;
|
|
msg.retry = 0;
|
|
|
|
if (msg.headers.hasOwnProperty('set-cookie')) {
|
|
msg.responseCookies = extractCookies(msg.headers['set-cookie']);
|
|
}
|
|
msg.headers['x-node-red-request-node'] = hashSum(msg.headers);
|
|
// msg.url = url; // revert when warning above finally removed
|
|
if (node.metric()) {
|
|
// Calculate request time
|
|
var diff = process.hrtime(preRequestTimestamp);
|
|
var ms = diff[0] * 1e3 + diff[1] * 1e-6;
|
|
var metricRequestDurationMillis = ms.toFixed(3);
|
|
node.metric("duration.millis", msg, metricRequestDurationMillis);
|
|
if (res.client && res.client.bytesRead) {
|
|
node.metric("size.bytes", msg, res.client.bytesRead);
|
|
}
|
|
}
|
|
|
|
// Convert the payload to the required return type
|
|
if (node.ret !== "bin") {
|
|
msg.payload = msg.payload.toString('utf8'); // txt
|
|
|
|
if (node.ret === "obj") {
|
|
try { msg.payload = JSON.parse(msg.payload); } // obj
|
|
catch(e) { node.warn(RED._("httpin.errors.json-error")); }
|
|
}
|
|
}
|
|
node.status({});
|
|
nodeSend(msg);
|
|
nodeDone();
|
|
}).catch(err => {
|
|
if(err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT') {
|
|
node.error(RED._("common.notification.errors.no-response"), msg);
|
|
node.status({fill:"red", shape:"ring", text:"common.notification.errors.no-response"});
|
|
}else{
|
|
node.error(err,msg);
|
|
node.status({fill:"red", shape:"ring", text:err.code});
|
|
}
|
|
msg.payload = err.toString() + " : " + url;
|
|
msg.statusCode = err.code || (err.response?err.response.statusCode:undefined);
|
|
nodeSend(msg);
|
|
nodeDone();
|
|
});
|
|
});
|
|
|
|
this.on("close",function() {
|
|
node.status({});
|
|
});
|
|
|
|
function extractCookies(setCookie) {
|
|
var cookies = {};
|
|
setCookie.forEach(function(c) {
|
|
var parsedCookie = cookie.parse(c);
|
|
var eq_idx = c.indexOf('=');
|
|
var key = c.substr(0, eq_idx).trim()
|
|
parsedCookie.value = parsedCookie[key];
|
|
delete parsedCookie[key];
|
|
cookies[key] = parsedCookie;
|
|
});
|
|
return cookies;
|
|
}
|
|
}
|
|
|
|
RED.nodes.registerType("http request",HTTPRequest,{
|
|
credentials: {
|
|
user: {type:"text"},
|
|
password: {type: "password"}
|
|
}
|
|
});
|
|
|
|
const md5 = (value) => { return crypto.createHash('md5').update(value).digest('hex') }
|
|
|
|
function ha1Compute(algorithm, user, realm, pass, nonce, cnonce) {
|
|
/**
|
|
* RFC 2617: handle both MD5 and MD5-sess algorithms.
|
|
*
|
|
* If the algorithm directive's value is "MD5" or unspecified, then HA1 is
|
|
* HA1=MD5(username:realm:password)
|
|
* If the algorithm directive's value is "MD5-sess", then HA1 is
|
|
* HA1=MD5(MD5(username:realm:password):nonce:cnonce)
|
|
*/
|
|
var ha1 = md5(user + ':' + realm + ':' + pass)
|
|
if (algorithm && algorithm.toLowerCase() === 'md5-sess') {
|
|
return md5(ha1 + ':' + nonce + ':' + cnonce)
|
|
} else {
|
|
return ha1
|
|
}
|
|
}
|
|
|
|
|
|
function buildDigestHeader(user, pass, method, path, authHeader) {
|
|
var challenge = {}
|
|
var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
|
|
for (;;) {
|
|
var match = re.exec(authHeader)
|
|
if (!match) {
|
|
break
|
|
}
|
|
challenge[match[1]] = match[2] || match[3]
|
|
}
|
|
var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth'
|
|
var nc = qop && '00000001'
|
|
var cnonce = qop && uuid().replace(/-/g, '')
|
|
var ha1 = ha1Compute(challenge.algorithm, user, challenge.realm, pass, challenge.nonce, cnonce)
|
|
var ha2 = md5(method + ':' + path)
|
|
var digestResponse = qop
|
|
? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2)
|
|
: md5(ha1 + ':' + challenge.nonce + ':' + ha2)
|
|
var authValues = {
|
|
username: user,
|
|
realm: challenge.realm,
|
|
nonce: challenge.nonce,
|
|
uri: path,
|
|
qop: qop,
|
|
response: digestResponse,
|
|
nc: nc,
|
|
cnonce: cnonce,
|
|
algorithm: challenge.algorithm,
|
|
opaque: challenge.opaque
|
|
}
|
|
|
|
authHeader = []
|
|
for (var k in authValues) {
|
|
if (authValues[k]) {
|
|
if (k === 'qop' || k === 'nc' || k === 'algorithm') {
|
|
authHeader.push(k + '=' + authValues[k])
|
|
} else {
|
|
authHeader.push(k + '="' + authValues[k] + '"')
|
|
}
|
|
}
|
|
}
|
|
authHeader = 'Digest ' + authHeader.join(', ')
|
|
return authHeader
|
|
}
|
|
}
|