1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge pull request #1913 from node-red-hitachi/dev-httpproxy

Add http-proxy for http-request node.
This commit is contained in:
Nick O'Leary 2018-10-19 11:10:26 +01:00 committed by GitHub
commit 34ead436b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 380 additions and 3 deletions

View File

@ -0,0 +1,136 @@
<!--
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.
-->
<script type="text/x-red" data-template-name="http proxy">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-config-input-name">
</div>
<div class="form-row">
<label for="node-config-input-url"><i class="fa fa-globe"></i> <span data-i18n="httpin.label.url"></span></label>
<input type="text" id="node-config-input-url" placeholder="http://hostname:port">
</div>
<div class="form-row">
<input type="checkbox" id="node-config-input-useAuth" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-useAuth" style="width: 70%;"><span data-i18n="httpin.use-proxyauth"></span></label>
<div style="margin-left: 20px" class="node-config-input-useAuth-row hide">
<div class="form-row">
<label for="node-config-input-username"><i class="fa fa-user"></i> <span data-i18n="common.label.username"></span></label>
<input type="text" id="node-config-input-username">
</div>
<div class="form-row">
<label for="node-config-input-password"><i class="fa fa-lock"></i> <span data-i18n="common.label.password"></span></label>
<input type="password" id="node-config-input-password">
</div>
</div>
</div>
<div class="form-row" style="margin-bottom:0;">
<label><i class="fa fa-list"></i> <span data-i18n="httpin.noproxy-hosts"></span></label>
</div>
<div class="form-row node-config-input-noproxy-container-row">
<ol id="node-config-input-noproxy-container"></ol>
</div>
</script>
<script type="text/x-red" data-help-name="http proxy">
<p>Configuration options for HTTP proxy.</p>
<h3>Details</h3>
<p>When accessing to the host in the ignored host list, no proxy will be used.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('http proxy', {
category: 'config',
defaults: {
name: {value:''},
url: {value:'',validate:function(v) { return (v && (v.indexOf('://') !== -1) && (v.trim().indexOf('http') === 0)); }},
noproxy: {value:[]}
},
credentials: {
username: {type:'text'},
password: {type:'password'}
},
label: function() {
return this.name || this.url || ('http proxy:' + this.id);
},
oneditprepare: function() {
$('#node-config-input-useAuth').change(function() {
if ($(this).is(":checked")) {
$('.node-config-input-useAuth-row').show();
} else {
$('.node-config-input-useAuth-row').hide();
$('#node-config-input-username').val('');
$('#node-config-input-password').val('');
}
});
if (this.credentials.username || this.credentials.has_password) {
$('#node-config-input-useAuth').prop('checked', true);
} else {
$('#node-config-input-useAuth').prop('checked', false);
}
$('#node-config-input-useAuth').change();
var hostList = $('#node-config-input-noproxy-container')
.css({'min-height':'150px','min-width':'450px'})
.editableList({
addItem: function(container, index, data) {
var row = $('<div/>')
.css({overflow: 'hidden',whiteSpace: 'nowrap'})
.appendTo(container);
var hostField = $('<input/>',{class:'node-config-input-host',type:'text',placeholder:'hostname'})
.css({width:'100%'})
.appendTo(row);
if (data.host) {
hostField.val(data.host);
}
},
removable: true
});
if (this.noproxy) {
for (var i in this.noproxy) {
hostList.editableList('addItem', {host:this.noproxy[i]});
}
}
if (hostList.editableList('items').length == 0) {
hostList.editableList('addItem', {host:''});
}
},
oneditsave: function() {
var hosts = $('#node-config-input-noproxy-container').editableList('items');
var node = this;
node.noproxy = [];
hosts.each(function(i) {
var host = $(this).find('.node-config-input-host').val().trim();
if (host) {
node.noproxy.push(host);
}
});
},
oneditresize: function(size) {
var rows = $('#node-config-dialog-edit-form>div:not(.node-config-input-noproxy-container-row)');
var height = size.height;
for (var i = 0; i < rows.size(); i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $('#node-config-dialog-edit-form>div.node-config-input-noproxy-container-row');
height -= (parseInt(editorRow.css('margin-top')) + parseInt(editorRow.css('margin-bottom')));
$('#node-config-input-noproxy-container').editableList('height',height);
}
});
</script>

View File

@ -0,0 +1,33 @@
/**
* 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';
function HTTPProxyConfig(n) {
RED.nodes.createNode(this, n);
this.name = n.name;
this.url = n.url;
this.noproxy = n.noproxy;
};
RED.nodes.registerType('http proxy', HTTPProxyConfig, {
credentials: {
username: {type:'text'},
password: {type:'password'}
}
});
};

View File

@ -53,6 +53,13 @@
</div>
</div>
<div class="form-row">
<input type="checkbox" id="node-input-useProxy" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-useProxy" style="width: auto;"><span data-i18n="httpin.use-proxy"></span></label>
<div id="node-input-useProxy-row" class="hide">
<label style="width: auto; margin-left: 20px; margin-right: 10px;" for="node-input-proxy"><i class="fa fa-globe"></i> <span data-i18n="httpin.proxy-config"></span></label><input type="text" style="width: 270px" id="node-input-proxy">
</div>
</div>
<div class="form-row">
<label for="node-input-ret"><i class="fa fa-arrow-left"></i> <span data-i18n="httpin.label.return"></span></label>
@ -112,7 +119,7 @@
url to be constructed using values of the incoming message. For example, if the url is set to
<code>example.com/{{{topic}}}</code>, it will have the value of <code>msg.topic</code> automatically inserted.
Using {{{...}}} prevents mustache from escaping characters like / & etc.</p>
<p><b>Note</b>: If running behind a proxy, the standard <code>http_proxy=...</code> environment variable should be set and Node-RED restarted.</p>
<p><b>Note</b>: If running behind a proxy, the standard <code>http_proxy=...</code> environment variable should be set and Node-RED restarted, or use Proxy Configuration. If Proxy Configuration was set, the configuration take precedence over environment variable.</p>
<h4>Using multiple HTTP Request nodes</h4>
<p>In order to use more than one of these nodes in the same flow, care must be taken with
the <code>msg.headers</code> property. The first node will set this property with
@ -141,7 +148,8 @@
method:{value:"GET"},
ret: {value:"txt"},
url:{value:"",validate:function(v) { return (v.trim().length === 0) || (v.indexOf("://") === -1) || (v.trim().indexOf("http") === 0)} },
tls: {type:"tls-config",required: false}
tls: {type:"tls-config",required: false},
proxy: {type:"http proxy",required: false}
},
credentials: {
user: {type:"text"},
@ -192,6 +200,24 @@
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
function updateProxyOptions() {
if ($("#node-input-useProxy").is(":checked")) {
$("#node-input-useProxy-row").show();
} else {
$("#node-input-useProxy-row").hide();
}
}
if (this.proxy) {
$("#node-input-useProxy").prop("checked", true);
} else {
$("#node-input-useProxy").prop("checked", false);
}
updateProxyOptions();
$("#node-input-useProxy").on("click", function() {
updateProxyOptions();
});
$("#node-input-ret").change(function() {
if ($("#node-input-ret").val() === "obj") {
$("#tip-json").show();
@ -204,6 +230,9 @@
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
if (!$("#node-input-useProxy").is(":checked")) {
$("#node-input-proxy").val("_ADD_");
}
}
});
</script>

View File

@ -41,6 +41,13 @@ module.exports = function(RED) {
if (process.env.no_proxy != null) { noprox = process.env.no_proxy.split(","); }
if (process.env.NO_PROXY != null) { 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) {
var preRequestTimestamp = process.hrtime();
node.status({fill:"blue",shape:"dot",text:"httpin.status.requesting"});
@ -84,6 +91,7 @@ module.exports = function(RED) {
opts.encoding = null; // Force NodeJs to return a Buffer (instead of a string)
opts.maxRedirects = 21;
opts.jar = request.jar();
opts.proxy = null;
var ctSet = "Content-Type"; // set default camel case
var clSet = "Content-Length";
if (msg.headers) {
@ -206,6 +214,15 @@ module.exports = function(RED) {
opts.proxy = null;
}
}
if (proxyConfig && proxyConfig.credentials && opts.proxy == proxyConfig.url) {
var proxyUsername = proxyConfig.credentials.username || '';
var proxyPassword = proxyConfig.credentials.password || '';
if (proxyUsername || proxyPassword) {
opts.headers['proxy-authorization'] =
'Basic ' +
Buffer.from(proxyUsername + ':' + proxyPassword).toString('base64');
}
}
if (tlsNode) {
tlsNode.addTLSOptions(opts);
} else {

View File

@ -382,6 +382,10 @@
"basicauth": "Use basic authentication",
"use-tls": "Enable secure (SSL/TLS) connection",
"tls-config":"TLS Configuration",
"use-proxy": "Use proxy",
"proxy-config": "Proxy Configuration",
"use-proxyauth": "Use proxy authentication",
"noproxy-hosts": "Ignore hosts",
"utf8": "a UTF-8 string",
"binary": "a binary buffer",
"json": "a parsed JSON object",

View File

@ -0,0 +1,22 @@
<!--
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.
-->
<script type="text/x-red" data-help-name="http proxy">
<p>プロキシのためのオプション設定</p>
<h3>詳細</h3>
<p>例外ホストに設定したホストにアクセスする際には、プロキシを使用しません。</p>
</script>

View File

@ -49,7 +49,7 @@
</dl>
<h3>詳細</h3>
<p>ードの設定でurlプロパティを指定する場合、<a href="http://mustache.github.io/mustache.5.html" target="_blank">mustache形式</a>のタグを含めることができます。これにより、URLを入力メッセージの値から構成することができます。例えば、urlが<code>example.com/{{{topic}}}</code>の場合、<code>msg.topic</code>の値による置き換えを自動的に行います。{{{...}}}表記を使うと、/、&といった文字をmustacheがエスケープするのを抑止できます。</p>
<p><b></b>: proxyサーバを利用している場合、環境変数<code>http_proxy=...</code>を設定して、Node-REDを再起動してください。</p>
<p><b></b>: proxyサーバを利用している場合、環境変数<code>http_proxy=...</code>を設定して、Node-REDを再起動するか、あるいはプロキシ設定をしてください。</p>
<h4>複数のHTTPリクエストードの利用</h4>
<p>同一フローで本ノードを複数利用するためには、<code>msg.headers</code>プロパティの扱いに注意しなくてはなりません。例えば、最初のノードがレスポンスヘッダにこのプロパティを設定し、次のノードがこのプロパティをリクエストヘッダに利用するというのは一般的には期待する動作ではありません。<code>msg.headers</code>プロパティをード間で変更しないままとすると、2つ目のードで無視されることになります。カスタムヘッダを設定するためには、<code>msg.headers</code>をまず削除もしくは空のオブジェクト<code>{}</code>にリセットします。
<h4>クッキーの扱い</h4>

View File

@ -382,6 +382,10 @@
"basicauth": "ベーシック認証を使用",
"use-tls": "SSL/TLS接続を有効化",
"tls-config": "TLS設定",
"use-proxy": "プロキシを使用",
"proxy-config": "プロキシ設定",
"use-proxyauth": "プロキシ認証を使用",
"noproxy-hosts": "例外ホスト",
"utf8": "文字列",
"binary": "バイナリバッファ",
"json": "JSON",

View File

@ -24,6 +24,7 @@ var stoppable = require('stoppable');
var helper = require("node-red-node-test-helper");
var httpRequestNode = require("nr-test-utils").require("@node-red/nodes/core/io/21-httprequest.js");
var tlsNode = require("nr-test-utils").require("@node-red/nodes/core/io/05-tls.js");
var httpProxyNode = require("nr-test-utils").require("@node-red/nodes/core/io/06-httpproxy.js");
var hashSum = require("hash-sum");
var httpProxy = require('http-proxy');
var cookieParser = require('cookie-parser');
@ -1194,6 +1195,80 @@ describe('HTTP Request Node', function() {
n1.receive({payload:"foo"});
});
});
it('should use http-proxy-config', function(done) {
var flow = [
{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect'),proxy:"n3"},
{id:"n2",type:"helper"},
{id:"n3",type:"http proxy",url:"http://localhost:" + testProxyPort}
];
var testNode = [ httpRequestNode, httpProxyNode ];
deleteProxySetting();
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('statusCode',200);
msg.payload.should.have.property('headers');
msg.payload.headers.should.have.property('x-testproxy-header','foobar');
done();
} catch(err) {
done(err);
}
});
n1.receive({payload:"foo"});
});
});
it('should not use http-proxy-config when invalid url is specified', function(done) {
var flow = [
{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect'),proxy:"n3"},
{id:"n2", type:"helper"},
{id:"n3",type:"http proxy",url:"invalidvalue"}
];
var testNode = [ httpRequestNode, httpProxyNode ];
deleteProxySetting();
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('statusCode',200);
msg.payload.should.have.property('headers');
msg.payload.headers.should.not.have.property('x-testproxy-header','foobar');
done();
} catch(err) {
done(err);
}
});
n1.receive({payload:"foo"});
});
});
it('should use http-proxy-config when valid noproxy is specified', function(done) {
var flow = [
{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect'),proxy:"n3"},
{id:"n2", type:"helper"},
{id:"n3",type:"http proxy",url:"http://localhost:" + testProxyPort,noproxy:["foo","localhost"]}
];
var testNode = [ httpRequestNode, httpProxyNode ];
deleteProxySetting();
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('statusCode',200);
msg.payload.headers.should.not.have.property('x-testproxy-header','foobar');
done();
} catch(err) {
done(err);
}
});
n1.receive({payload:"foo"});
});
});
});
describe('authentication', function() {
@ -1264,6 +1339,63 @@ describe('HTTP Request Node', function() {
n1.receive({payload:"foo"});
});
});
it('should authenticate on proxy server(http-proxy-config)', function(done) {
var flow = [
{id:"n1",type:"http request", wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/proxyAuthenticate'),proxy:"n3"},
{id:"n2", type:"helper"},
{id:"n3",type:"http proxy",url:"http://localhost:" + testProxyPort}
];
var testNode = [ httpRequestNode, httpProxyNode ];
deleteProxySetting();
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var n3 = helper.getNode("n3");
n3.credentials = {username:'foouser', password:'barpassword'};
n2.on("input", function(msg) {
try {
msg.should.have.property('statusCode',200);
msg.payload.should.have.property('user', 'foouser');
msg.payload.should.have.property('pass', 'barpassword');
msg.payload.should.have.property('headers');
msg.payload.headers.should.have.property('x-testproxy-header','foobar');
done();
} catch(err) {
done(err);
}
});
n1.receive({payload:"foo"});
});
});
it('should output an error when proxy authentication was failed(http-proxy-config)', function(done) {
var flow = [
{id:"n1",type:"http request", wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/proxyAuthenticate'),proxy:"n3"},
{id:"n2", type:"helper"},
{id:"n3",type:"http proxy",url:"http://@localhost:" + testProxyPort}
];
var testNode = [ httpRequestNode, httpProxyNode ];
deleteProxySetting();
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var n3 = helper.getNode("n3");
n3.credentials = {username:'xxxuser', password:'barpassword'};
n2.on("input", function(msg) {
try {
msg.should.have.property('statusCode',407);
msg.headers.should.have.property('proxy-authenticate', 'BASIC realm="test"');
msg.payload.should.have.property('headers');
msg.payload.headers.should.have.property('x-testproxy-header','foobar');
done();
} catch(err) {
done(err);
}
});
n1.receive({payload:"foo"});
});
});
});
describe('redirect-cookie', function() {