mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
Merge branch 'dev' into export-module-info
This commit is contained in:
@@ -227,34 +227,42 @@
|
||||
name: {value:""},
|
||||
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v, opt) {
|
||||
if (!v || v.length === 0) { return true }
|
||||
const errors = []
|
||||
for (var i=0;i<v.length;i++) {
|
||||
if (/^\${[^}]+}$/.test(v[i].v)) {
|
||||
// Allow ${ENV_VAR} value
|
||||
continue
|
||||
}
|
||||
if (/msg|flow|global/.test(v[i].vt)) {
|
||||
if (!RED.utils.validatePropertyExpression(v[i].v)) {
|
||||
return RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v });
|
||||
errors.push(RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v }))
|
||||
}
|
||||
} else if (v[i].vt === "jsonata") {
|
||||
try{ jsonata(v[i].v); }
|
||||
catch(e){
|
||||
return RED._("node-red:inject.errors.invalid-jsonata", { prop: 'msg.'+v[i].p, error: e.message });
|
||||
errors.push(RED._("node-red:inject.errors.invalid-jsonata", { prop: 'msg.'+v[i].p, error: e.message }))
|
||||
}
|
||||
} else if (v[i].vt === "json") {
|
||||
try{ JSON.parse(v[i].v); }
|
||||
catch(e){
|
||||
return RED._("node-red:inject.errors.invalid-json", { prop: 'msg.'+v[i].p, error: e.message });
|
||||
errors.push(RED._("node-red:inject.errors.invalid-json", { prop: 'msg.'+v[i].p, error: e.message }))
|
||||
}
|
||||
} else if (v[i].vt === "num"){
|
||||
if (!/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/.test(v[i].v)) {
|
||||
return RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v });
|
||||
errors.push(RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v }))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
return errors
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
repeat: {
|
||||
value:"", validate: function(v, opt) {
|
||||
if ((v === "") ||
|
||||
(RED.validators.number(v) &&
|
||||
(RED.validators.number()(v) &&
|
||||
(v >= 0) && (v <= 2147483))) {
|
||||
return true;
|
||||
}
|
||||
@@ -263,7 +271,7 @@
|
||||
},
|
||||
crontab: {value:""},
|
||||
once: {value:false},
|
||||
onceDelay: {value:0.1},
|
||||
onceDelay: {value:0.1, validate: RED.validators.number(true)},
|
||||
topic: {value:""},
|
||||
payload: {value:"", validate: RED.validators.typedInput("payloadType", false) },
|
||||
payloadType: {value:"date"},
|
||||
|
@@ -378,7 +378,7 @@
|
||||
return { id: id, label: RED.nodes.workspace(id).label } //flow id + name
|
||||
} else {
|
||||
const instanceNode = RED.nodes.node(id)
|
||||
const pathLabel = (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8)).name)
|
||||
const pathLabel = (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8))?.name || instanceNode.type)
|
||||
return { id: id, label: pathLabel }
|
||||
}
|
||||
})
|
||||
|
@@ -194,27 +194,46 @@
|
||||
nodeMap[node.links[i]].new = true;
|
||||
}
|
||||
}
|
||||
var n;
|
||||
for (var id in nodeMap) {
|
||||
|
||||
let editHistories = []
|
||||
let n;
|
||||
for (let id in nodeMap) {
|
||||
if (nodeMap.hasOwnProperty(id)) {
|
||||
n = RED.nodes.node(id);
|
||||
if (n) {
|
||||
editHistories.push({
|
||||
t:'edit',
|
||||
node: n,
|
||||
changes: {
|
||||
links: [...n.links]
|
||||
},
|
||||
changed: n.changed
|
||||
})
|
||||
if (nodeMap[id].old && !nodeMap[id].new) {
|
||||
// Removed id
|
||||
i = n.links.indexOf(node.id);
|
||||
if (i > -1) {
|
||||
n.links.splice(i,1);
|
||||
n.changed = true
|
||||
n.dirty = true
|
||||
}
|
||||
} else if (!nodeMap[id].old && nodeMap[id].new) {
|
||||
// Added id
|
||||
i = n.links.indexOf(id);
|
||||
if (i === -1) {
|
||||
n.links.push(node.id);
|
||||
n.changed = true
|
||||
n.dirty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (editHistories.length > 0) {
|
||||
return {
|
||||
history: editHistories
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onAdd() {
|
||||
@@ -254,13 +273,14 @@
|
||||
onEditPrepare(this,"link out");
|
||||
},
|
||||
oneditsave: function() {
|
||||
onEditSave(this);
|
||||
const result = onEditSave(this);
|
||||
// In case the name has changed, ensure any link call nodes on this
|
||||
// tab are redrawn with the updated name
|
||||
var localCallNodes = RED.nodes.filterNodes({z:RED.workspaces.active(), type:"link call"});
|
||||
localCallNodes.forEach(function(node) {
|
||||
node.dirty = true;
|
||||
});
|
||||
return result
|
||||
},
|
||||
onadd: onAdd,
|
||||
oneditresize: resizeNodeList
|
||||
@@ -329,7 +349,7 @@
|
||||
onEditPrepare(this,"link in");
|
||||
},
|
||||
oneditsave: function() {
|
||||
onEditSave(this);
|
||||
return onEditSave(this);
|
||||
},
|
||||
oneditresize: resizeNodeList
|
||||
});
|
||||
@@ -373,7 +393,7 @@
|
||||
|
||||
},
|
||||
oneditsave: function() {
|
||||
onEditSave(this);
|
||||
return onEditSave(this);
|
||||
},
|
||||
onadd: onAdd,
|
||||
oneditresize: resizeNodeList
|
||||
|
@@ -25,19 +25,19 @@ module.exports = function(RED) {
|
||||
function sendResults(node,send,_msgid,msgs,cloneFirstMessage) {
|
||||
if (msgs == null) {
|
||||
return;
|
||||
} else if (!util.isArray(msgs)) {
|
||||
} else if (!Array.isArray(msgs)) {
|
||||
msgs = [msgs];
|
||||
}
|
||||
var msgCount = 0;
|
||||
for (var m=0; m<msgs.length; m++) {
|
||||
if (msgs[m]) {
|
||||
if (!util.isArray(msgs[m])) {
|
||||
if (!Array.isArray(msgs[m])) {
|
||||
msgs[m] = [msgs[m]];
|
||||
}
|
||||
for (var n=0; n < msgs[m].length; n++) {
|
||||
var msg = msgs[m][n];
|
||||
if (msg !== null && msg !== undefined) {
|
||||
if (typeof msg === 'object' && !Buffer.isBuffer(msg) && !util.isArray(msg)) {
|
||||
if (typeof msg === 'object' && !Buffer.isBuffer(msg) && !Array.isArray(msg)) {
|
||||
if (msgCount === 0 && cloneFirstMessage !== false) {
|
||||
msgs[m][n] = RED.util.cloneMessage(msgs[m][n]);
|
||||
msg = msgs[m][n];
|
||||
@@ -47,7 +47,7 @@ module.exports = function(RED) {
|
||||
} else {
|
||||
var type = typeof msg;
|
||||
if (type === 'object') {
|
||||
type = Buffer.isBuffer(msg)?'Buffer':(util.isArray(msg)?'Array':'Date');
|
||||
type = Buffer.isBuffer(msg)?'Buffer':(Array.isArray(msg)?'Array':'Date');
|
||||
}
|
||||
node.error(RED._("function.error.non-message-returned",{ type: type }));
|
||||
}
|
||||
@@ -374,7 +374,7 @@ module.exports = function(RED) {
|
||||
iniOpt.breakOnSigint = true;
|
||||
}
|
||||
}
|
||||
node.script = vm.createScript(functionText, createVMOpt(node, ""));
|
||||
node.script = new vm.Script(functionText, createVMOpt(node, ""));
|
||||
if (node.fin && (node.fin !== "")) {
|
||||
var finText = `(function () {
|
||||
var node = {
|
||||
@@ -438,10 +438,9 @@ module.exports = function(RED) {
|
||||
|
||||
//store the error in msg to be used in flows
|
||||
msg.error = err;
|
||||
|
||||
var line = 0;
|
||||
var errorMessage;
|
||||
if (stack.length > 0) {
|
||||
let line = 0;
|
||||
let errorMessage;
|
||||
while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
|
||||
line++;
|
||||
}
|
||||
@@ -455,11 +454,13 @@ module.exports = function(RED) {
|
||||
errorMessage += " (line "+lineno+", col "+cha+")";
|
||||
}
|
||||
}
|
||||
if (errorMessage) {
|
||||
err.message = errorMessage
|
||||
}
|
||||
}
|
||||
if (!errorMessage) {
|
||||
errorMessage = err.toString();
|
||||
}
|
||||
done(errorMessage);
|
||||
// Pass the whole error object so any additional properties
|
||||
// (such as cause) are preserved
|
||||
done(err);
|
||||
}
|
||||
else if (typeof err === "string") {
|
||||
done(err);
|
||||
|
@@ -233,9 +233,12 @@ module.exports = function(RED) {
|
||||
// only replace if they match exactly
|
||||
RED.util.setMessageProperty(msg,property,value);
|
||||
} else {
|
||||
// if target is boolean then just replace it
|
||||
if (rule.tot === "bool") { current = value; }
|
||||
else { current = current.replace(fromRE,value); }
|
||||
current = current.replace(fromRE,value);
|
||||
if (rule.tot === "bool" && current === ""+value) {
|
||||
// If the target type is boolean, and the replace call has resulted in "true"/"false",
|
||||
// convert to boolean type (which 'value' already is)
|
||||
current = value
|
||||
}
|
||||
RED.util.setMessageProperty(msg,property,current);
|
||||
}
|
||||
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
|
||||
|
@@ -20,6 +20,7 @@ module.exports = function(RED) {
|
||||
var exec = require('child_process').exec;
|
||||
var fs = require('fs');
|
||||
var isUtf8 = require('is-utf8');
|
||||
const isWindows = process.platform === 'win32'
|
||||
|
||||
function ExecNode(n) {
|
||||
RED.nodes.createNode(this,n);
|
||||
@@ -85,9 +86,12 @@ module.exports = function(RED) {
|
||||
}
|
||||
});
|
||||
var cmd = arg.shift();
|
||||
// Since 18.20.2/20.12.2, it is invalid to call spawn on Windows with a .bat/.cmd file
|
||||
// without using shell: true.
|
||||
const opts = isWindows ? { ...node.spawnOpt, shell: true } : node.spawnOpt
|
||||
/* istanbul ignore else */
|
||||
node.debug(cmd+" ["+arg+"]");
|
||||
child = spawn(cmd,arg,node.spawnOpt);
|
||||
child = spawn(cmd,arg,opts);
|
||||
node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid});
|
||||
var unknownCommand = (child.pid === undefined);
|
||||
if (node.timer !== 0) {
|
||||
|
@@ -16,6 +16,7 @@
|
||||
|
||||
module.exports = function(RED) {
|
||||
"use strict";
|
||||
const { getProxyForUrl } = require('./lib/proxyHelper');
|
||||
var mqtt = require("mqtt");
|
||||
var isUtf8 = require('is-utf8');
|
||||
var HttpsProxyAgent = require('https-proxy-agent');
|
||||
@@ -617,17 +618,8 @@ module.exports = function(RED) {
|
||||
// Only for ws or wss, check if proxy env var for additional configuration
|
||||
if (node.brokerurl.indexOf("wss://") > -1 || node.brokerurl.indexOf("ws://") > -1) {
|
||||
// check if proxy is set in env
|
||||
let prox, noprox, noproxy;
|
||||
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(","); }
|
||||
if (noprox) {
|
||||
for (var i = 0; i < noprox.length; i += 1) {
|
||||
if (node.brokerurl.indexOf(noprox[i].trim()) !== -1) { noproxy = true; }
|
||||
}
|
||||
}
|
||||
if (prox && !noproxy) {
|
||||
const prox = getProxyForUrl(node.brokerurl, RED.settings.proxyOptions);
|
||||
if (prox) {
|
||||
var parsedUrl = url.parse(node.brokerurl);
|
||||
var proxyOpts = url.parse(prox);
|
||||
// true for wss
|
||||
|
@@ -16,6 +16,7 @@
|
||||
|
||||
module.exports = async function(RED) {
|
||||
"use strict";
|
||||
const { getProxyForUrl, parseUrl } = require('./lib/proxyHelper');
|
||||
const { got } = await import('got')
|
||||
const {CookieJar} = require("tough-cookie");
|
||||
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
|
||||
@@ -101,18 +102,18 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
|
||||
node.insecureHTTPParser = n.insecureHTTPParser
|
||||
|
||||
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;
|
||||
let proxyConfig = n.proxy ? RED.nodes.getNode(n.proxy) || {} : null
|
||||
const getProxy = (url) => {
|
||||
const proxyOptions = Object.assign({}, RED.settings.proxyOptions);
|
||||
if (n.proxy && proxyConfig) {
|
||||
proxyOptions.env = {
|
||||
no_proxy: (proxyConfig.noproxy || []).join(','),
|
||||
http_proxy: (proxyConfig.url)
|
||||
}
|
||||
}
|
||||
return getProxyForUrl(url, proxyOptions)
|
||||
}
|
||||
let prox = getProxy(nodeUrl || '')
|
||||
|
||||
let timingLog = false;
|
||||
if (RED.settings.hasOwnProperty("httpRequestTimingLog")) {
|
||||
@@ -141,7 +142,15 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} headersObject
|
||||
* @param {string} name
|
||||
* @return {any} value
|
||||
*/
|
||||
const getHeaderValue = (headersObject, name) => {
|
||||
const asLowercase = name.toLowercase();
|
||||
return headersObject[Object.keys(headersObject).find(k => k.toLowerCase() === asLowercase)];
|
||||
}
|
||||
this.on("input",function(msg,nodeSend,nodeDone) {
|
||||
checkNodeAgentPatch();
|
||||
//reset redirectList on each request
|
||||
@@ -177,7 +186,11 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
url = "http://"+url;
|
||||
}
|
||||
}
|
||||
|
||||
// before any parameters are appended to the `url`, lets check see if the proxy needs a refresh
|
||||
let proxyUrl = prox; // The proxyUrl determined for `nodeUrl`
|
||||
if(url !== nodeUrl) {
|
||||
proxyUrl = getProxy(url)
|
||||
}
|
||||
// The Request module used in Node-RED 1.x was tolerant of query strings that
|
||||
// were partially encoded. For example - "?a=hello%20there&b=20%"
|
||||
// The GOT module doesn't like that.
|
||||
@@ -521,49 +534,43 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
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 (!opts.headers.hasOwnProperty('user-agent')) {
|
||||
opts.headers['user-agent'] = 'Mozilla/5.0 (Node-RED)';
|
||||
}
|
||||
if (prox && !noproxy) {
|
||||
var match = prox.match(/^(https?:\/\/)?(.+)?:([0-9]+)?/i);
|
||||
if (proxyUrl) {
|
||||
const match = proxyUrl.match(/^(https?:\/\/)?(.+)?:([0-9]+)?/i);
|
||||
if (match) {
|
||||
let proxyAgent;
|
||||
let proxyURL = new URL(prox);
|
||||
const proxyURL = parseUrl(proxyUrl)
|
||||
//set username/password to null to stop empty creds header
|
||||
/** @type {import('hpagent').HttpProxyAgentOptions} */
|
||||
let proxyOptions = {
|
||||
proxy: {
|
||||
protocol: proxyURL.protocol,
|
||||
hostname: proxyURL.hostname,
|
||||
port: proxyURL.port,
|
||||
username: null,
|
||||
password: null
|
||||
},
|
||||
proxy: proxyUrl,
|
||||
scheduling: 'lifo',
|
||||
maxFreeSockets: 256,
|
||||
maxSockets: 256,
|
||||
keepAlive: true
|
||||
}
|
||||
if (proxyConfig && proxyConfig.credentials) {
|
||||
let proxyUsername = proxyConfig.credentials.username || '';
|
||||
let proxyPassword = proxyConfig.credentials.password || '';
|
||||
const proxyUsername = proxyConfig.credentials.username || '';
|
||||
const proxyPassword = proxyConfig.credentials.password || '';
|
||||
if (proxyUsername || proxyPassword) {
|
||||
proxyOptions.proxy = proxyURL
|
||||
proxyOptions.proxy.username = proxyUsername;
|
||||
proxyOptions.proxy.password = proxyPassword;
|
||||
}
|
||||
} else if (proxyURL.username || proxyURL.password){
|
||||
proxyOptions.proxy.username = proxyURL.username;
|
||||
proxyOptions.proxy.password = proxyURL.password;
|
||||
proxyOptions.proxy = proxyURL
|
||||
// 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)
|
||||
}
|
||||
https: new HttpProxyAgent(proxyOptions)
|
||||
};
|
||||
|
||||
} else {
|
||||
node.warn("Bad proxy url: "+ prox);
|
||||
node.warn("Bad proxy url: " + proxyUrl);
|
||||
}
|
||||
}
|
||||
if (useKeepAlive && !opts.agent) {
|
||||
|
@@ -40,6 +40,99 @@
|
||||
|
||||
(function() {
|
||||
|
||||
const headerTypes = [
|
||||
/*
|
||||
{ value: "Accept", label: "Accept", hasValue: false },
|
||||
{ value: "Accept-Encoding", label: "Accept-Encoding", hasValue: false },
|
||||
{ value: "Accept-Language", label: "Accept-Language", hasValue: false },
|
||||
*/
|
||||
{ value: "Authorization", label: "Authorization", hasValue: false },
|
||||
/*
|
||||
{ value: "Content-Type", label: "Content-Type", hasValue: false },
|
||||
{ value: "Cache-Control", label: "Cache-Control", hasValue: false },
|
||||
*/
|
||||
{ value: "User-Agent", label: "User-Agent", hasValue: false },
|
||||
/*
|
||||
{ value: "Location", label: "Location", hasValue: false },
|
||||
*/
|
||||
{ value: "other", label: RED._("node-red:httpin.label.other"),
|
||||
hasValue: true, icon: "red/images/typedInput/az.svg" },
|
||||
]
|
||||
|
||||
const headerOptions = {};
|
||||
const defaultOptions = [
|
||||
{ value: "other", label: RED._("node-red:httpin.label.other"),
|
||||
hasValue: true, icon: "red/images/typedInput/az.svg" },
|
||||
"env",
|
||||
];
|
||||
/*
|
||||
headerOptions["accept"] = [
|
||||
{ value: "text/plain", label: "text/plain", hasValue: false },
|
||||
{ value: "text/html", label: "text/html", hasValue: false },
|
||||
{ value: "application/json", label: "application/json", hasValue: false },
|
||||
{ value: "application/xml", label: "application/xml", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
|
||||
headerOptions["accept-encoding"] = [
|
||||
{ value: "gzip", label: "gzip", hasValue: false },
|
||||
{ value: "deflate", label: "deflate", hasValue: false },
|
||||
{ value: "compress", label: "compress", hasValue: false },
|
||||
{ value: "br", label: "br", hasValue: false },
|
||||
{ value: "gzip, deflate", label: "gzip, deflate", hasValue: false },
|
||||
{ value: "gzip, deflate, br", label: "gzip, deflate, br", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
headerOptions["accept-language"] = [
|
||||
{ value: "*", label: "*", hasValue: false },
|
||||
{ value: "en-GB, en-US, en;q=0.9", label: "en-GB, en-US, en;q=0.9", hasValue: false },
|
||||
{ value: "de-AT, de-DE;q=0.9, en;q=0.5", label: "de-AT, de-DE;q=0.9, en;q=0.5", hasValue: false },
|
||||
{ value: "es-mx,es,en;q=0.5", label: "es-mx,es,en;q=0.5", hasValue: false },
|
||||
{ value: "fr-CH, fr;q=0.9, en;q=0.8", label: "fr-CH, fr;q=0.9, en;q=0.8", hasValue: false },
|
||||
{ value: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", label: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", hasValue: false },
|
||||
{ value: "ja-JP, jp", label: "ja-JP, jp", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
headerOptions["content-type"] = [
|
||||
{ value: "text/css", label: "text/css", hasValue: false },
|
||||
{ value: "text/plain", label: "text/plain", hasValue: false },
|
||||
{ value: "text/html", label: "text/html", hasValue: false },
|
||||
{ value: "application/json", label: "application/json", hasValue: false },
|
||||
{ value: "application/octet-stream", label: "application/octet-stream", hasValue: false },
|
||||
{ value: "application/pdf", label: "application/pdf", hasValue: false },
|
||||
{ value: "application/xml", label: "application/xml", hasValue: false },
|
||||
{ value: "application/zip", label: "application/zip", hasValue: false },
|
||||
{ value: "multipart/form-data", label: "multipart/form-data", hasValue: false },
|
||||
{ value: "audio/aac", label: "audio/aac", hasValue: false },
|
||||
{ value: "audio/ac3", label: "audio/ac3", hasValue: false },
|
||||
{ value: "audio/basic", label: "audio/basic", hasValue: false },
|
||||
{ value: "audio/mp4", label: "audio/mp4", hasValue: false },
|
||||
{ value: "audio/ogg", label: "audio/ogg", hasValue: false },
|
||||
{ value: "image/bmp", label: "image/bmp", hasValue: false },
|
||||
{ value: "image/gif", label: "image/gif", hasValue: false },
|
||||
{ value: "image/jpeg", label: "image/jpeg", hasValue: false },
|
||||
{ value: "image/png", label: "image/png", hasValue: false },
|
||||
{ value: "image/tiff", label: "image/tiff", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
headerOptions["cache-control"] = [
|
||||
{ value: "max-age=0", label: "max-age=0", hasValue: false },
|
||||
{ value: "max-age=86400", label: "max-age=86400", hasValue: false },
|
||||
{ value: "no-cache", label: "no-cache", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
*/
|
||||
headerOptions["user-agent"] = [
|
||||
{ value: "Mozilla/5.0", label: "Mozilla/5.0", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
|
||||
function getHeaderOptions(headerName) {
|
||||
const lc = (headerName || "").toLowerCase();
|
||||
let opts = headerOptions[lc];
|
||||
return opts || defaultOptions;
|
||||
}
|
||||
|
||||
function ws_oneditprepare() {
|
||||
$("#websocket-client-row").hide();
|
||||
$("#node-input-mode").on("change", function() {
|
||||
@@ -192,7 +285,8 @@
|
||||
value: "",
|
||||
label:RED._("node-red:websocket.sendheartbeat"),
|
||||
validate: RED.validators.number(/*blank allowed*/true) },
|
||||
subprotocol: {value:"",required: false}
|
||||
subprotocol: {value:"",required: false},
|
||||
headers: { value: [] }
|
||||
},
|
||||
inputs:0,
|
||||
outputs:0,
|
||||
@@ -200,6 +294,9 @@
|
||||
return this.path;
|
||||
},
|
||||
oneditprepare: function() {
|
||||
|
||||
const node = this;
|
||||
|
||||
$("#node-config-input-path").on("change keyup paste",function() {
|
||||
$(".node-config-row-tls").toggle(/^wss:/i.test($(this).val()))
|
||||
});
|
||||
@@ -214,14 +311,114 @@
|
||||
if (!heartbeatActive) {
|
||||
$("#node-config-input-hb").val("");
|
||||
}
|
||||
|
||||
const hasMatch = function (arr, value) {
|
||||
return arr.some(function (ht) {
|
||||
return ht.value === value
|
||||
});
|
||||
}
|
||||
|
||||
const headerList = $("#node-input-headers-container").css('min-height', '150px').css('min-width', '450px').editableList({
|
||||
addItem: function (container, i, header) {
|
||||
const row = $('<div/>').css({
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex'
|
||||
}).appendTo(container);
|
||||
const propertNameCell = $('<div/>').css({ 'flex-grow': 1 }).appendTo(row);
|
||||
const propertyName = $('<input/>', { class: "node-input-header-name", type: "text", style: "width: 100%" })
|
||||
.appendTo(propertNameCell)
|
||||
.typedInput({ types: headerTypes });
|
||||
|
||||
const propertyValueCell = $('<div/>').css({ 'flex-grow': 1, 'margin-left': '10px' }).appendTo(row);
|
||||
const propertyValue = $('<input/>', { class: "node-input-header-value", type: "text", style: "width: 100%" })
|
||||
.appendTo(propertyValueCell)
|
||||
.typedInput({
|
||||
types: getHeaderOptions(header.keyType)
|
||||
});
|
||||
|
||||
const setup = function(_header) {
|
||||
const headerTypeIsAPreset = function(h) {return hasMatch(headerTypes, h) };
|
||||
const headerValueIsAPreset = function(h, v) {return hasMatch(getHeaderOptions(h), v) };
|
||||
|
||||
const {keyType, keyValue, valueType, valueValue} = header;
|
||||
|
||||
if(keyType == "other") {
|
||||
propertyName.typedInput('type', keyType);
|
||||
propertyName.typedInput('value', keyValue);
|
||||
} else if (headerTypeIsAPreset(keyType)) {
|
||||
propertyName.typedInput('type', keyType);
|
||||
} else {
|
||||
propertyName.typedInput('type', "other");
|
||||
propertyName.typedInput('value', keyValue);
|
||||
}
|
||||
|
||||
if(valueType == "other" || valueType == "env" ) {
|
||||
propertyValue.typedInput('type', valueType);
|
||||
propertyValue.typedInput('value', valueValue);
|
||||
} else if (headerValueIsAPreset(propertyName.typedInput('type'), valueType)) {
|
||||
propertyValue.typedInput('type', valueType);
|
||||
} else {
|
||||
propertyValue.typedInput('type', "other");
|
||||
propertyValue.typedInput('value', valueValue);
|
||||
}
|
||||
}
|
||||
setup(header);
|
||||
|
||||
propertyName.on('change', function (event) {
|
||||
propertyValue.typedInput('types', getHeaderOptions(propertyName.typedInput('type')));
|
||||
});
|
||||
|
||||
},
|
||||
sortable: true,
|
||||
removable: true
|
||||
});
|
||||
if (node.headers) {
|
||||
for (let index = 0; index < node.headers.length; index++) {
|
||||
const element = node.headers[index];
|
||||
headerList.editableList('addItem', node.headers[index]);
|
||||
}
|
||||
}
|
||||
},
|
||||
oneditsave: function() {
|
||||
|
||||
const node = this;
|
||||
|
||||
if (!/^wss:/i.test($("#node-config-input-path").val())) {
|
||||
$("#node-config-input-tls").val("_ADD_");
|
||||
}
|
||||
if (!$("#node-config-input-hb-cb").prop("checked")) {
|
||||
$("#node-config-input-hb").val("0");
|
||||
}
|
||||
|
||||
const headers = $("#node-input-headers-container").editableList('items');
|
||||
|
||||
node.headers = [];
|
||||
headers.each(function(i) {
|
||||
const header = $(this);
|
||||
const keyType = header.find(".node-input-header-name").typedInput('type');
|
||||
const keyValue = header.find(".node-input-header-name").typedInput('value');
|
||||
const valueType = header.find(".node-input-header-value").typedInput('type');
|
||||
const valueValue = header.find(".node-input-header-value").typedInput('value');
|
||||
node.headers.push({
|
||||
keyType, keyValue, valueType, valueValue
|
||||
})
|
||||
|
||||
});
|
||||
},
|
||||
oneditresize: function(size) {
|
||||
const dlg = $("#dialog-form");
|
||||
const expandRow = dlg.find('.node-input-headers-container-row');
|
||||
let height = dlg.height() - 5;
|
||||
if(expandRow && expandRow.length){
|
||||
const siblingRows = dlg.find('> .form-row:not(.node-input-headers-container-row)');
|
||||
for (let i = 0; i < siblingRows.size(); i++) {
|
||||
const cr = $(siblingRows[i]);
|
||||
if(cr.is(":visible"))
|
||||
height -= cr.outerHeight(true);
|
||||
}
|
||||
$("#node-input-headers-container").editableList('height',height);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -299,8 +496,15 @@
|
||||
<span data-i18n="inject.seconds"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<label><i class="fa fa-list"></i> <span data-i18n="httpin.label.headers"></span></label>
|
||||
</div>
|
||||
<div class="form-row node-input-headers-container-row">
|
||||
<ol id="node-input-headers-container"></ol>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
<p><span data-i18n="[html]websocket.tip.url1"></span></p>
|
||||
<span data-i18n="[html]websocket.tip.url2"></span>
|
||||
<p><span data-i18n="[html]websocket.tip.url2"></span></p>
|
||||
<span data-i18n="[html]websocket.tip.headers"></span>
|
||||
</div>
|
||||
</script>
|
||||
|
@@ -20,7 +20,7 @@ module.exports = function(RED) {
|
||||
var inspect = require("util").inspect;
|
||||
var url = require("url");
|
||||
var HttpsProxyAgent = require('https-proxy-agent');
|
||||
|
||||
const { getProxyForUrl } = require('./lib/proxyHelper');
|
||||
|
||||
var serverUpgradeAdded = false;
|
||||
function handleServerUpgrade(request, socket, head) {
|
||||
@@ -58,6 +58,7 @@ module.exports = function(RED) {
|
||||
node.isServer = !/^ws{1,2}:\/\//i.test(node.path);
|
||||
node.closing = false;
|
||||
node.tls = n.tls;
|
||||
node.upgradeHeaders = n.headers
|
||||
|
||||
if (n.hb) {
|
||||
var heartbeat = parseInt(n.hb);
|
||||
@@ -68,21 +69,9 @@ module.exports = function(RED) {
|
||||
|
||||
function startconn() { // Connect to remote endpoint
|
||||
node.tout = null;
|
||||
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 noproxy = false;
|
||||
if (noprox) {
|
||||
for (var i in noprox) {
|
||||
if (node.path.indexOf(noprox[i].trim()) !== -1) { noproxy=true; }
|
||||
}
|
||||
}
|
||||
|
||||
var agent = undefined;
|
||||
if (prox && !noproxy) {
|
||||
const prox = getProxyForUrl(node.brokerurl, RED.settings.proxyOptions);
|
||||
let agent = undefined;
|
||||
if (prox) {
|
||||
agent = new HttpsProxyAgent(prox);
|
||||
}
|
||||
|
||||
@@ -96,6 +85,42 @@ module.exports = function(RED) {
|
||||
tlsNode.addTLSOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
// We need to check if undefined, to guard against previous installs, that will not have had this property set (applies to 3.1.x setups)
|
||||
// Else this will be breaking potentially
|
||||
if(node.upgradeHeaders !== undefined && node.upgradeHeaders.length > 0){
|
||||
options.headers = {};
|
||||
for(let i = 0;i<node.upgradeHeaders.length;i++){
|
||||
const header = node.upgradeHeaders[i];
|
||||
const keyType = header.keyType;
|
||||
const keyValue = header.keyValue;
|
||||
const valueType = header.valueType;
|
||||
const valueValue = header.valueValue;
|
||||
|
||||
const headerName = keyType === 'other' ? keyValue : keyType;
|
||||
let headerValue;
|
||||
|
||||
switch(valueType){
|
||||
case 'other':
|
||||
headerValue = valueValue;
|
||||
break;
|
||||
|
||||
case 'env':
|
||||
headerValue = RED.util.evaluateNodeProperty(valueValue,valueType,node);
|
||||
break;
|
||||
|
||||
default:
|
||||
headerValue = valueType;
|
||||
break;
|
||||
}
|
||||
|
||||
if(headerName && headerValue){
|
||||
options.headers[headerName] = headerValue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var socket = new ws(node.path,node.subprotocol,options);
|
||||
socket.setMaxListeners(0);
|
||||
node.server = socket; // keep for closing
|
||||
|
@@ -1,257 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
**/
|
||||
var util = require("util");
|
||||
var mqtt = require("mqtt");
|
||||
var events = require("events");
|
||||
|
||||
util.log("[warn] nodes/core/io/lib/mqtt.js is deprecated and will be removed in a future release of Node-RED. Please report this usage to the Node-RED mailing list.");
|
||||
|
||||
//var inspect = require("util").inspect;
|
||||
|
||||
//var Client = module.exports.Client = function(
|
||||
|
||||
var port = 1883;
|
||||
var host = "localhost";
|
||||
|
||||
function MQTTClient(port,host) {
|
||||
this.port = port||1883;
|
||||
this.host = host||"localhost";
|
||||
this.messageId = 1;
|
||||
this.pendingSubscriptions = {};
|
||||
this.inboundMessages = {};
|
||||
this.lastOutbound = (new Date()).getTime();
|
||||
this.lastInbound = (new Date()).getTime();
|
||||
this.connected = false;
|
||||
|
||||
this._nextMessageId = function() {
|
||||
this.messageId += 1;
|
||||
if (this.messageId > 0xFFFF) {
|
||||
this.messageId = 1;
|
||||
}
|
||||
return this.messageId;
|
||||
}
|
||||
events.EventEmitter.call(this);
|
||||
}
|
||||
util.inherits(MQTTClient, events.EventEmitter);
|
||||
|
||||
MQTTClient.prototype.connect = function(options) {
|
||||
if (!this.connected) {
|
||||
var self = this;
|
||||
options = options||{};
|
||||
self.options = options;
|
||||
self.options.keepalive = options.keepalive||15;
|
||||
self.options.clean = self.options.clean||true;
|
||||
self.options.protocolId = 'MQIsdp';
|
||||
self.options.protocolVersion = 3;
|
||||
|
||||
self.client = mqtt.createConnection(this.port,this.host,function(err,client) {
|
||||
if (err) {
|
||||
self.connected = false;
|
||||
clearInterval(self.watchdog);
|
||||
self.connectionError = true;
|
||||
//util.log('[mqtt] ['+self.uid+'] connection error 1 : '+inspect(err));
|
||||
self.emit('connectionlost',err);
|
||||
return;
|
||||
}
|
||||
client.on('close',function(e) {
|
||||
//util.log('[mqtt] ['+self.uid+'] on close');
|
||||
clearInterval(self.watchdog);
|
||||
if (!self.connectionError) {
|
||||
if (self.connected) {
|
||||
self.connected = false;
|
||||
self.emit('connectionlost',e);
|
||||
} else {
|
||||
self.emit('disconnect');
|
||||
}
|
||||
}
|
||||
});
|
||||
client.on('error',function(e) {
|
||||
//util.log('[mqtt] ['+self.uid+'] on error : '+inspect(e));
|
||||
clearInterval(self.watchdog);
|
||||
if (self.connected) {
|
||||
self.connected = false;
|
||||
self.emit('connectionlost',e);
|
||||
}
|
||||
});
|
||||
client.on('connack',function(packet) {
|
||||
if (packet.returnCode === 0) {
|
||||
self.watchdog = setInterval(function(self) {
|
||||
var now = (new Date()).getTime();
|
||||
|
||||
//util.log('[mqtt] ['+self.uid+'] watchdog '+inspect({connected:self.connected,connectionError:self.connectionError,pingOutstanding:self.pingOutstanding,now:now,lastOutbound:self.lastOutbound,lastInbound:self.lastInbound}));
|
||||
|
||||
if (now - self.lastOutbound > self.options.keepalive*500 || now - self.lastInbound > self.options.keepalive*500) {
|
||||
if (self.pingOutstanding) {
|
||||
//util.log('[mqtt] ['+self.uid+'] watchdog pingOustanding - disconnect');
|
||||
try {
|
||||
self.client.disconnect();
|
||||
} catch (err) {
|
||||
}
|
||||
} else {
|
||||
//util.log('[mqtt] ['+self.uid+'] watchdog pinging');
|
||||
self.lastOutbound = (new Date()).getTime();
|
||||
self.lastInbound = (new Date()).getTime();
|
||||
self.pingOutstanding = true;
|
||||
self.client.pingreq();
|
||||
}
|
||||
}
|
||||
|
||||
},self.options.keepalive*500,self);
|
||||
self.pingOutstanding = false;
|
||||
self.lastInbound = (new Date()).getTime()
|
||||
self.lastOutbound = (new Date()).getTime()
|
||||
self.connected = true;
|
||||
self.connectionError = false;
|
||||
self.emit('connect');
|
||||
} else {
|
||||
self.connected = false;
|
||||
self.emit('connectionlost');
|
||||
}
|
||||
});
|
||||
client.on('suback',function(packet) {
|
||||
self.lastInbound = (new Date()).getTime()
|
||||
var topic = self.pendingSubscriptions[packet.messageId];
|
||||
self.emit('subscribe',topic,packet.granted[0]);
|
||||
delete self.pendingSubscriptions[packet.messageId];
|
||||
});
|
||||
client.on('unsuback',function(packet) {
|
||||
self.lastInbound = (new Date()).getTime()
|
||||
var topic = self.pendingSubscriptions[packet.messageId];
|
||||
self.emit('unsubscribe',topic);
|
||||
delete self.pendingSubscriptions[packet.messageId];
|
||||
});
|
||||
client.on('publish',function(packet) {
|
||||
self.lastInbound = (new Date()).getTime();
|
||||
if (packet.qos < 2) {
|
||||
var p = packet;
|
||||
self.emit('message',p.topic,p.payload,p.qos,p.retain);
|
||||
} else {
|
||||
self.inboundMessages[packet.messageId] = packet;
|
||||
this.lastOutbound = (new Date()).getTime()
|
||||
self.client.pubrec(packet);
|
||||
}
|
||||
if (packet.qos == 1) {
|
||||
this.lastOutbound = (new Date()).getTime()
|
||||
self.client.puback(packet);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('pubrel',function(packet) {
|
||||
self.lastInbound = (new Date()).getTime()
|
||||
var p = self.inboundMessages[packet.messageId];
|
||||
if (p) {
|
||||
self.emit('message',p.topic,p.payload,p.qos,p.retain);
|
||||
delete self.inboundMessages[packet.messageId];
|
||||
}
|
||||
self.lastOutbound = (new Date()).getTime()
|
||||
self.client.pubcomp(packet);
|
||||
});
|
||||
|
||||
client.on('puback',function(packet) {
|
||||
self.lastInbound = (new Date()).getTime()
|
||||
// outbound qos-1 complete
|
||||
});
|
||||
|
||||
client.on('pubrec',function(packet) {
|
||||
self.lastInbound = (new Date()).getTime()
|
||||
self.lastOutbound = (new Date()).getTime()
|
||||
self.client.pubrel(packet);
|
||||
});
|
||||
client.on('pubcomp',function(packet) {
|
||||
self.lastInbound = (new Date()).getTime()
|
||||
// outbound qos-2 complete
|
||||
});
|
||||
client.on('pingresp',function(packet) {
|
||||
//util.log('[mqtt] ['+self.uid+'] received pingresp');
|
||||
self.lastInbound = (new Date()).getTime()
|
||||
self.pingOutstanding = false;
|
||||
});
|
||||
|
||||
this.lastOutbound = (new Date()).getTime()
|
||||
this.connectionError = false;
|
||||
client.connect(self.options);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MQTTClient.prototype.subscribe = function(topic,qos) {
|
||||
var self = this;
|
||||
if (self.connected) {
|
||||
var options = {
|
||||
subscriptions:[{topic:topic,qos:qos}],
|
||||
messageId: self._nextMessageId()
|
||||
};
|
||||
this.pendingSubscriptions[options.messageId] = topic;
|
||||
this.lastOutbound = (new Date()).getTime();
|
||||
self.client.subscribe(options);
|
||||
self.client.setPacketEncoding('binary');
|
||||
}
|
||||
}
|
||||
MQTTClient.prototype.unsubscribe = function(topic) {
|
||||
var self = this;
|
||||
if (self.connected) {
|
||||
var options = {
|
||||
unsubscriptions:[topic],
|
||||
messageId: self._nextMessageId()
|
||||
};
|
||||
this.pendingSubscriptions[options.messageId] = topic;
|
||||
this.lastOutbound = (new Date()).getTime()
|
||||
self.client.unsubscribe(options);
|
||||
}
|
||||
}
|
||||
|
||||
MQTTClient.prototype.publish = function(topic,payload,qos,retain) {
|
||||
var self = this;
|
||||
if (self.connected) {
|
||||
|
||||
if (!Buffer.isBuffer(payload)) {
|
||||
if (typeof payload === "object") {
|
||||
payload = JSON.stringify(payload);
|
||||
} else if (typeof payload !== "string") {
|
||||
payload = ""+payload;
|
||||
}
|
||||
}
|
||||
var options = {
|
||||
topic: topic,
|
||||
payload: payload,
|
||||
qos: qos||0,
|
||||
retain:retain||false
|
||||
};
|
||||
if (options.qos !== 0) {
|
||||
options.messageId = self._nextMessageId();
|
||||
}
|
||||
this.lastOutbound = (new Date()).getTime()
|
||||
self.client.publish(options);
|
||||
}
|
||||
}
|
||||
|
||||
MQTTClient.prototype.disconnect = function() {
|
||||
var self = this;
|
||||
if (this.connected) {
|
||||
this.connected = false;
|
||||
try {
|
||||
this.client.disconnect();
|
||||
} catch(err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
MQTTClient.prototype.isConnected = function() {
|
||||
return this.connected;
|
||||
}
|
||||
module.exports.createClient = function(port,host) {
|
||||
var mqtt_client = new MQTTClient(port,host);
|
||||
return mqtt_client;
|
||||
}
|
219
packages/node_modules/@node-red/nodes/core/network/lib/proxyHelper.js
vendored
Normal file
219
packages/node_modules/@node-red/nodes/core/network/lib/proxyHelper.js
vendored
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
Copyright (C) 2016-2018 Rob Wu <rob@robwu.nl>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
*/
|
||||
|
||||
/*
|
||||
This proxy helper is heavily based on the proxy helper from Rob Wu as detailed above.
|
||||
It has been modified to work with the Node-RED runtime environment.
|
||||
The license for the original code is reproduced above.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Parse a URL into its components.
|
||||
* @param {String} url The URL to parse
|
||||
* @returns {URL}
|
||||
*/
|
||||
const parseUrl = (url) => {
|
||||
let parsedUrl = {
|
||||
protocol: null,
|
||||
host: null,
|
||||
port: null,
|
||||
hostname: null,
|
||||
query: null,
|
||||
href: null
|
||||
}
|
||||
try {
|
||||
if (!url) { return parsedUrl }
|
||||
parsedUrl = new URL(url)
|
||||
} catch (error) {
|
||||
// dont throw error
|
||||
}
|
||||
return parsedUrl
|
||||
}
|
||||
|
||||
const DEFAULT_PORTS = {
|
||||
ftp: 21,
|
||||
gopher: 70,
|
||||
http: 80,
|
||||
https: 443,
|
||||
ws: 80,
|
||||
wss: 443,
|
||||
mqtt: 1880,
|
||||
mqtts: 8883
|
||||
}
|
||||
|
||||
const modeOverride = getEnv('NR_PROXY_MODE', {})
|
||||
|
||||
/**
|
||||
* @typedef {Object} ProxyOptions
|
||||
* @property {'strict'|'legacy'} [mode] - Legacy mode is for non-strict previous proxy determination logic (for node-red <= v3.1 compatibility) (default 'strict')
|
||||
* @property {boolean} [favourUpperCase] - Favour UPPER_CASE *_PROXY env vars (default false)
|
||||
* @property {boolean} [lowerCaseOnly] - Prevent UPPER_CASE *_PROXY env vars being used. (default false)
|
||||
* @property {boolean} [excludeNpm] - Prevent npm_config_*_proxy env vars being used. (default false)
|
||||
* @property {object} [env] - The environment object to use (defaults to process.env)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the proxy URL for a given URL.
|
||||
* @param {string|URL} url - The URL, or the result from url.parse.
|
||||
* @param {ProxyOptions} [options] - The options object (optional)
|
||||
* @return {string} The URL of the proxy that should handle the request to the
|
||||
* given URL. If no proxy is set, this will be an empty string.
|
||||
*/
|
||||
function getProxyForUrl(url, options) {
|
||||
url = url || ''
|
||||
const defaultOptions = {
|
||||
mode: 'strict',
|
||||
lowerCaseOnly: false,
|
||||
favourUpperCase: false,
|
||||
excludeNpm: false,
|
||||
}
|
||||
options = Object.assign({}, defaultOptions, options)
|
||||
|
||||
if (modeOverride === 'legacy' || modeOverride === 'strict') {
|
||||
options.mode = modeOverride
|
||||
}
|
||||
|
||||
if (options.mode === 'legacy') {
|
||||
return legacyGetProxyForUrl(url, options.env || process.env)
|
||||
}
|
||||
|
||||
const parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {}
|
||||
let proto = parsedUrl.protocol
|
||||
let hostname = parsedUrl.host
|
||||
let port = parsedUrl.port
|
||||
if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') {
|
||||
return '' // Don't proxy URLs without a valid scheme or host.
|
||||
}
|
||||
|
||||
proto = proto.split(':', 1)[0]
|
||||
// Stripping ports in this way instead of using parsedUrl.hostname to make
|
||||
// sure that the brackets around IPv6 addresses are kept.
|
||||
hostname = hostname.replace(/:\d*$/, '')
|
||||
port = parseInt(port) || DEFAULT_PORTS[proto] || 0
|
||||
if (!shouldProxy(hostname, port, options)) {
|
||||
return '' // Don't proxy URLs that match NO_PROXY.
|
||||
}
|
||||
|
||||
let proxy =
|
||||
getEnv('npm_config_' + proto + '_proxy', options) ||
|
||||
getEnv(proto + '_proxy', options) ||
|
||||
getEnv('npm_config_proxy', options) ||
|
||||
getEnv('all_proxy', options)
|
||||
if (proxy && proxy.indexOf('://') === -1) {
|
||||
// Missing scheme in proxy, default to the requested URL's scheme.
|
||||
proxy = proto + '://' + proxy
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the proxy URL for a given URL.
|
||||
* For node-red < v3.1 or compatibility mode
|
||||
* @param {string} url The URL to check for proxying
|
||||
* @param {object} [env] The environment object to use (default process.env)
|
||||
* @returns
|
||||
*/
|
||||
function legacyGetProxyForUrl(url, env) {
|
||||
env = env || process.env
|
||||
let prox, noprox;
|
||||
if (env.http_proxy) { prox = env.http_proxy; }
|
||||
if (env.HTTP_PROXY) { prox = env.HTTP_PROXY; }
|
||||
if (env.no_proxy) { noprox = env.no_proxy.split(","); }
|
||||
if (env.NO_PROXY) { noprox = env.NO_PROXY.split(","); }
|
||||
|
||||
let noproxy = false;
|
||||
if (noprox) {
|
||||
for (let i in noprox) {
|
||||
if (url.indexOf(noprox[i].trim()) !== -1) { noproxy=true; }
|
||||
}
|
||||
}
|
||||
if (prox && !noproxy) {
|
||||
return prox
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a given URL should be proxied.
|
||||
*
|
||||
* @param {string} hostname - The host name of the URL.
|
||||
* @param {number} port - The effective port of the URL.
|
||||
* @returns {boolean} Whether the given URL should be proxied.
|
||||
* @private
|
||||
*/
|
||||
function shouldProxy(hostname, port, options) {
|
||||
const NO_PROXY =
|
||||
(getEnv('npm_config_no_proxy', options) || getEnv('no_proxy', options)).toLowerCase()
|
||||
if (!NO_PROXY) {
|
||||
return true // Always proxy if NO_PROXY is not set.
|
||||
}
|
||||
if (NO_PROXY === '*') {
|
||||
return false // Never proxy if wildcard is set.
|
||||
}
|
||||
|
||||
return NO_PROXY.split(/[,\s]/).every(function (proxy) {
|
||||
if (!proxy) {
|
||||
return true // Skip zero-length hosts.
|
||||
}
|
||||
const parsedProxy = proxy.match(/^(.+):(\d+)$/)
|
||||
let parsedProxyHostname = parsedProxy ? parsedProxy[1] : proxy
|
||||
const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0
|
||||
if (parsedProxyPort && parsedProxyPort !== port) {
|
||||
return true // Skip if ports don't match.
|
||||
}
|
||||
|
||||
if (!/^[.*]/.test(parsedProxyHostname)) {
|
||||
// No wildcards, so stop proxying if there is an exact match.
|
||||
return hostname !== parsedProxyHostname
|
||||
}
|
||||
|
||||
if (parsedProxyHostname.charAt(0) === '*') {
|
||||
// Remove leading wildcard.
|
||||
parsedProxyHostname = parsedProxyHostname.slice(1)
|
||||
}
|
||||
// Stop proxying if the hostname ends with the no_proxy host.
|
||||
return !hostname.endsWith(parsedProxyHostname)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for an environment constiable.
|
||||
*
|
||||
* @param {string} key - The name of the environment constiable.
|
||||
* @param {ProxyOptions} options - The name of the environment constiable.
|
||||
* @return {string} The value of the environment constiable.
|
||||
* @private
|
||||
*/
|
||||
function getEnv(key, options) {
|
||||
const env = (options && options.env) || process.env
|
||||
if (options && options.excludeNpm === true) {
|
||||
if (key.startsWith('npm_config_')) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
if (options && options.lowerCaseOnly === true) {
|
||||
return env[key.toLowerCase()] || ''
|
||||
} else if (options && options.favourUpperCase === true) {
|
||||
return env[key.toUpperCase()] || env[key.toLowerCase()] || ''
|
||||
}
|
||||
return env[key.toLowerCase()] || env[key.toUpperCase()] || ''
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getProxyForUrl,
|
||||
parseUrl
|
||||
}
|
Reference in New Issue
Block a user