blob: 0bab0523b55b146b2c2a8870a62412255af0fa0b [file] [log] [blame]
// ============LICENSE_START===============================================
// Copyright (C) 2021 Nordix Foundation. All rights reserved.
// ========================================================================
// 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.
// ============LICENSE_END=================================================
// Basic http/https proxy
// Call the the proxy on 8080/8433 for http/https
// The destination (proxied) protocol may be http or https
// Proxy healthcheck on 8081/8434 for http/https - answers with statistics in json
const http = require('http');
const net = require('net');
const urlp = require('url');
const process = require('process')
const https = require('https');
const fs = require('fs');
// Proxy server port for http
const proxyport = 8080;
// Proxy server port for https
const proxyporthttps = 8433;
// Proyx server alive check, port for http
const aliveport = 8081;
// Proyx server alive check, port for https
const aliveporthttps = 8434;
// Default https destination port
const defaulthttpsport = "443";
var debug = false;
// Certs etc for https
const httpsoptions = {
key: fs.readFileSync('cert/key.crt'),
cert: fs.readFileSync('cert/cert.crt'),
passphrase: fs.readFileSync('cert/pass', 'utf8')
};
const stats = {
'http-requests-initiated': 0,
'http-requests-failed': 0,
'https-requests-initiated': 0,
'https-requests-failed': 0
};
// handle a http proxy request
function httpclientrequest(clientrequest, clientresponse) {
stats['http-requests-initiated']++;
// Extract destination information
var crurl=clientrequest.url;
var crhost=clientrequest.headers['host'];
var crproto=clientrequest.headers['x-forwarded-proto'];
if (debug) {
console.log("crurl: "+crurl)
console.log("crhost: "+crhost)
console.log("crproto: "+crproto)
}
// If this server is running behind a proxy (like istio envoy proxy) then the 'clientrequest.url'
// only contains the path component (i.e /test ). The host name and port is included in the
// 'host' header and the protocol (http/https) is in the header 'x-forwarded-proto'.
// In case of istio - https to a pod over mTLS does not seem to work. Only http.
// Othewise, if no front proxy, the full url is included in the 'clientrequest.url'
if (crproto != undefined) {
crurl=crproto+"://"+crhost+crurl
if (debug) {
console.log(" Constructed ulr: "+crurl)
}
} else if (crurl.startsWith('/')) {
console.log("Catched bad url in http request: "+crurl)
clientresponse.end();
return;
}
const clientrequesturl = new URL(crurl);
var proxyrequestoptions = {
'host': clientrequesturl.hostname,
'port': clientrequesturl.port,
'method': clientrequest.method,
'path': clientrequesturl.pathname+clientrequesturl.search,
'agent': clientrequest.agent,
'auth': clientrequest.auth,
'headers': clientrequest.headers
};
// Setup connection to destination
var proxyrequest = http.request(
proxyrequestoptions,
function (proxyresponse) {
clientresponse.writeHead(proxyresponse.statusCode, proxyresponse.headers);
proxyresponse.on('data', function (chunk) {
clientresponse.write(chunk);
});
proxyresponse.on('end', function () {
clientresponse.end();
});
}
);
// Handle the connection and data transfer between source and desitnation
proxyrequest.on('error', function (error) {
clientresponse.writeHead(500);
stats['http-requests-failed']++;
console.log(error);
clientresponse.write("<h1>500 Error</h1>\r\n" + "<p>Error was <pre>" + error + "</pre></p>\r\n" + "</body></html>\r\n");
clientresponse.end();
});
clientrequest.addListener('data', function (chunk) {
proxyrequest.write(chunk);
});
clientrequest.addListener('end', function () {
proxyrequest.end();
});
}
// Function to add a 'connect' message listener to a http server
function addhttpsconnect(httpserver) {
httpserver.addListener(
'connect',
function (request, socketrequest, bodyhead) {
if (debug) {
console.log("Received 'connect' for: "+request['url'])
}
stats['https-requests-initiated']++;
// Extract destination information
var res = request['url'].split(":")
var hostname = res[0]
var port = defaulthttpsport;
if (res[1] != null) {
port = res[1]
}
// Setup connection to destination
var httpversion = request['httpVersion'];
var proxysocket = new net.Socket();
proxysocket.connect(
parseInt(port), hostname,
function () {
proxysocket.write(bodyhead);
socketrequest.write("HTTP/" + httpversion + " 200 Connection established\r\n\r\n");
}
);
// Handle the connection and data transfer between source and desitnation
proxysocket.on('data', function (chunk) {
socketrequest.write(chunk);
});
proxysocket.on('end', function () {
socketrequest.end();
});
socketrequest.on('data', function (chunk) {
proxysocket.write(chunk);
});
socketrequest.on('end', function () {
proxysocket.end();
});
proxysocket.on('error', function (err) {
stats['https-requests-failed']++;
console.log(err);
socketrequest.write("HTTP/" + httpversion + " 500 Connection error\r\n\r\n");
socketrequest.end();
});
socketrequest.on('error', function (err) {
stats['https-requests-failed']++;
console.log(err);
proxysocket.end();
});
}
);
}
function main() {
// -------------------- Alive server ----------------------------------
// Responde with '200' and statistics for any path (except for GET|PUT|DELETE on /debug) on the alive address
const alivelistener = function (req, res) {
if (req.url == "/debug") {
if (req.method == "GET") {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write(""+debug)
res.end()
return
} else if (req.method == "PUT") {
debug=true
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write("OK")
res.end()
return
} else if (req.method == "DELETE") {
debug=false
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write("OK")
res.end()
return
}
}
console.log(stats)
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(JSON.stringify(stats))
res.end();
};
// The alive server - for healthckeck
const aliveserver = http.createServer(alivelistener);
// The alive server - for healthckeck
const aliveserverhttps = https.createServer(httpsoptions, alivelistener);
//Handle heatlhcheck requests
aliveserver.listen(aliveport, () => {
console.log('alive server on: '+aliveport);
console.log(' example: curl localhost:'+aliveport)
});
//Handle heatlhcheck requests
aliveserverhttps.listen(aliveporthttps, () => {
console.log('alive server on: '+aliveporthttps);
console.log(' example: curl -k https://localhost:'+aliveporthttps)
});
// -------------------- Proxy server ---------------------------------
// The proxy server
const proxyserver = http.createServer(httpclientrequest).listen(proxyport);
console.log('http/https proxy for http proxy calls on port ' + proxyport);
console.log(' example: curl --proxy http://localhost:8080 http://100.110.120.130:1234')
console.log(' example: curl -k --proxy http//localhost:8080 https://100.110.120.130:5678')
// handle a http proxy request - https listener
addhttpsconnect(proxyserver);
const proxyserverhttps = https.createServer(httpsoptions, httpclientrequest).listen(proxyporthttps);
console.log('http/https proxy for https proxy calls on port ' + proxyporthttps);
console.log(' example: curl --proxy-insecure --proxy https://localhost:8433 http://100.110.120.130:1234')
console.log(' example: curl -k --proxy-insecure --proxy https://localhost:8433 https://100.110.120.130:5678')
// handle a https proxy request - https listener
addhttpsconnect(proxyserverhttps);
}
//Handle ctrl c when running in interactive mode
process.on('SIGINT', () => {
console.info("Interrupted")
process.exit(0)
})
main();