React Router Paths, SNICallback, POST, GET - node.js

I have built the following express app to serve multiple sites with HTTPS from a single server:
// strict
'use strict';
// vars
const tls = require('tls');
var fs = require('fs');
var http = require('http');
var https = require('https');
var express = require('express');
var vhost = require('vhost');
var forceSSL = require('express-force-ssl');
var cors = require('cors');
var app = express();
var server;
var secureServer;
// always force HTTP -> HTTPS redirect
app.use(forceSSL);
// allow cross origin
app.use(cors());
// Express settings (main switch logic - each host, and each static directory)
var siteOne = vhost('siteone.com', express.static('../siteone/dist'));
var wwwSiteOne = vhost('www.siteone.com', express.static('../siteone/dist'));
var siteTwo = vhost('sitetwo.com', express.static('../sitetwo/dist'));
var wwwSiteTwo = vhost('www.sitetwo.com', express.static('../sitetwo/dist'));
app.use(cors()); // allow cross origin
app.use(siteOne); // all hosts defined above
app.use(wwwSiteOne);
app.use(siteTwo);
app.use(wwwSiteTwo);
app.use(function(req, res, next) { // 404 page as last "use" call
res.status(404).send('404 page :(');
});
// HTTP Server - http.createServer is enough for our HTTP needs
server = http.createServer(app);
server.listen(8080); // router is set to forward port 80 (http requests) to port 8080
// HTTPS Server - use virtual host to redirect with secureserver
var secureContext = {
'siteone.com': tls.createSecureContext({
key: fs.readFileSync('../../sslcert/siteone/privkey.pem', 'utf8'),
cert: fs.readFileSync('../../sslcert/siteone/fullchain.pem', 'utf8'),
ca: fs.readFileSync('../../sslcert/siteone/chain.pem', 'utf8'),
}),
'www.siteone.com': tls.createSecureContext({
key: fs.readFileSync('../../sslcert/siteone/privkey.pem', 'utf8'),
cert: fs.readFileSync('../../sslcert/siteone/fullchain.pem', 'utf8'),
ca: fs.readFileSync('../../sslcert/siteone/chain.pem', 'utf8'),
}),
'sitetwo.com': tls.createSecureContext({
key: fs.readFileSync('../../sslcert/sitetwo/privkey.pem', 'utf8'),
cert: fs.readFileSync('../../sslcert/sitetwo/fullchain.pem', 'utf8'),
ca: fs.readFileSync('../../sslcert/sitetwo/chain.pem', 'utf8'),
}),
'www.sitetwo.com': tls.createSecureContext({
key: fs.readFileSync('../../sslcert/sitetwo/privkey.pem', 'utf8'),
cert: fs.readFileSync('../../sslcert/sitetwo/fullchain.pem', 'utf8'),
ca: fs.readFileSync('../../sslcert/sitetwo/chain.pem', 'utf8'),
}),
}
try {
var options = {
SNICallback: function (domain, cb) {
if (secureContext[domain]) {
if (cb) {
cb(null, secureContext[domain]);
} else {
// compatibility for older versions of node
return secureContext[domain];
}
} else {
console.log('Doing nothing. Domain requsted: ' + domain);
}
},
// must list a default key and cert because required by tls.createServer()
key: fs.readFileSync('../../sslcert/siteone/privkey.pem'),
cert: fs.readFileSync('../../sslcert/siteone/fullchain.pem'),
}
secureServer = https.createServer(options, app);
secureServer.listen(8043); // router is set to forward port 443 (https requests) to port 8043
} catch (err){
console.error(err.message);
console.error(err.stack);
}
So far, I have run into issues with POST, GET, and the react router (v4) paths. For example, a single page site with react router works to every link as long as the the user starts from the homepage, but if the user provides a link directly in the url bar (ex. siteone.com/somecoolpath) i get the 404 page from the website switch.
Similiarly I have a POST from a site for adding a user's email to a DB that is on url path /add_email... and I always get a 404 when I send the email...
All these paths work on their own apps, but not when I serve through this SNICallback switch.
Things I have tried but didn't work:
wildcards in the vhost:
var reactRouterSite = vhost('siteone.com*', express.static('../siteone/dist'));
rewriting the domain in the SNI callback:
SNICallback: function (domain, cb) {
if (domain.includes('siteone')) { // any request from siteone
domain = 'siteone.com'
}
...
a final switch() statement in the last app.use() statement:
app.use(function(req, res, next) {
console.log(req);
switch (req.url) {
case '/somecoolpath':
return siteOne;
break;
default:
res.status(404).send('404 page :(');
}
});
I've spent hours searching around for possible solutions, but haven't even found an example of a case with react router with SNICallback... any ideas what I could try? Or is there a much easier solution?
all the sites are hosted in other node instances, express apps on ports 8081, 8082, etc. For all sites the root pages load as expected through this 'switch', it's just those GET/POST urls and the copy/pasting router urls that don't work.

One thing you can do is get rid of the vhosts middleware and just create a app.get('*') route and do the logic yourself checking req.hostname and instead of using static directories, render a template. I've done this with multiple domains and multiple react apps.

From dzm's response, I ended up indeed taking out the vhost middleware, and instead used an app.get('*' ...) route, but instead of rendering a template, I found that a proxy would work best for my needs, and used the http-proxy package. After some tests, the react router paths can be copy/pasted directly in the browser and work as expected, and the GET/POST methods work as well, as long as the app.get() and app.post() paths exist also in the website switch.
Replacing the vhosts / app.use() section, while leaving the other parts of the code i.e. the SNICallback, and secureContext untouched, the solutions looks like this:
// set up proxy server
var httpProxy = require('http-proxy');
var proxy = httpProxy.createProxyServer({}); // leave options empty --> custom logic below in app.get('*' ...)
// GET paths
app.get('*', function(req, res) {
// custom logic to handle and proxy the request
console.log(req.hostname);
switch (req.hostname) {
case 'siteone.com':
proxy.web(req, res, { target: 'http://127.0.0.1:8081' }); // different node server running on local port 8081
break;
case 'sitetwo.com':
proxy.web(req, res, { target: 'http://127.0.0.1:8082' }); // different node server running on local port 8082
break;
default:
res.status(404).send('404 page :(');
break;
}
});
// POST paths (for siteone)
app.post('/new_email', function(req, res) {
proxy.web(req, res, { target: 'http://127.0.0.1:8081' }); // this post path must also be written on the index.js of siteone.com
});

Related

Backend API calls not rerouting in node express [duplicate]

To avoid same-domain AJAX issues, I want my node.js web server to forward all requests from URL /api/BLABLA to another server, for example other_domain.com:3000/BLABLA, and return to user the same thing that this remote server returned, transparently.
All other URLs (beside /api/*) are to be served directly, no proxying.
How do I achieve this with node.js + express.js? Can you give a simple code example?
(both the web server and the remote 3000 server are under my control, both running node.js with express.js)
So far I found this https://github.com/http-party/node-http-proxy , but reading the documentation there didn't make me any wiser. I ended up with
var proxy = new httpProxy.RoutingProxy();
app.all("/api/*", function(req, res) {
console.log("old request url " + req.url)
req.url = '/' + req.url.split('/').slice(2).join('/'); // remove the '/api' part
console.log("new request url " + req.url)
proxy.proxyRequest(req, res, {
host: "other_domain.com",
port: 3000
});
});
but nothing is returned to the original web server (or to the end user), so no luck.
request has been deprecated as of February 2020, I'll leave the answer below for historical reasons, but please consider moving to an alternative listed in this issue.
Archive
I did something similar but I used request instead:
var request = require('request');
app.get('/', function(req,res) {
//modify the url in any way you want
var newurl = 'http://google.com/';
request(newurl).pipe(res);
});
I found a shorter and very straightforward solution which works seamlessly, and with authentication as well, using express-http-proxy:
const url = require('url');
const proxy = require('express-http-proxy');
// New hostname+path as specified by question:
const apiProxy = proxy('other_domain.com:3000/BLABLA', {
proxyReqPathResolver: req => url.parse(req.baseUrl).path
});
And then simply:
app.use('/api/*', apiProxy);
Note: as mentioned by #MaxPRafferty, use req.originalUrl in place of baseUrl to preserve the querystring:
forwardPath: req => url.parse(req.baseUrl).path
Update: As mentioned by Andrew (thank you!), there's a ready-made solution using the same principle:
npm i --save http-proxy-middleware
And then:
const proxy = require('http-proxy-middleware')
var apiProxy = proxy('/api', {target: 'http://www.example.org/api'});
app.use(apiProxy)
Documentation: http-proxy-middleware on Github
You want to use http.request to create a similar request to the remote API and return its response.
Something like this:
const http = require('http');
// or use import http from 'http';
/* your app config here */
app.post('/api/BLABLA', (oreq, ores) => {
const options = {
// host to forward to
host: 'www.google.com',
// port to forward to
port: 80,
// path to forward to
path: '/api/BLABLA',
// request method
method: 'POST',
// headers to send
headers: oreq.headers,
};
const creq = http
.request(options, pres => {
// set encoding
pres.setEncoding('utf8');
// set http status code based on proxied response
ores.writeHead(pres.statusCode);
// wait for data
pres.on('data', chunk => {
ores.write(chunk);
});
pres.on('close', () => {
// closed, let's end client request as well
ores.end();
});
pres.on('end', () => {
// finished, let's finish client request as well
ores.end();
});
})
.on('error', e => {
// we got an error
console.log(e.message);
try {
// attempt to set error message and http status
ores.writeHead(500);
ores.write(e.message);
} catch (e) {
// ignore
}
ores.end();
});
creq.end();
});
Notice: I haven't really tried the above, so it might contain parse errors hopefully this will give you a hint as to how to get it to work.
To extend trigoman's answer (full credits to him) to work with POST (could also make work with PUT etc):
app.use('/api', function(req, res) {
var url = 'YOUR_API_BASE_URL'+ req.url;
var r = null;
if(req.method === 'POST') {
r = request.post({uri: url, json: req.body});
} else {
r = request(url);
}
req.pipe(r).pipe(res);
});
I used the following setup to direct everything on /rest to my backend server (on port 8080), and all other requests to the frontend server (a webpack server on port 3001). It supports all HTTP-methods, doesn't lose any request meta-info and supports websockets (which I need for hot reloading)
var express = require('express');
var app = express();
var httpProxy = require('http-proxy');
var apiProxy = httpProxy.createProxyServer();
var backend = 'http://localhost:8080',
frontend = 'http://localhost:3001';
app.all("/rest/*", function(req, res) {
apiProxy.web(req, res, {target: backend});
});
app.all("/*", function(req, res) {
apiProxy.web(req, res, {target: frontend});
});
var server = require('http').createServer(app);
server.on('upgrade', function (req, socket, head) {
apiProxy.ws(req, socket, head, {target: frontend});
});
server.listen(3000);
First install express and http-proxy-middleware
npm install express http-proxy-middleware --save
Then in your server.js
const express = require('express');
const proxy = require('http-proxy-middleware');
const app = express();
app.use(express.static('client'));
// Add middleware for http proxying
const apiProxy = proxy('/api', { target: 'http://localhost:8080' });
app.use('/api', apiProxy);
// Render your site
const renderIndex = (req, res) => {
res.sendFile(path.resolve(__dirname, 'client/index.html'));
}
app.get('/*', renderIndex);
app.listen(3000, () => {
console.log('Listening on: http://localhost:3000');
});
In this example we serve the site on port 3000, but when a request end with /api we redirect it to localhost:8080.
http://localhost:3000/api/login redirect to http://localhost:8080/api/login
Ok, here's a ready-to-copy-paste answer using the require('request') npm module and an environment variable *instead of an hardcoded proxy):
coffeescript
app.use (req, res, next) ->
r = false
method = req.method.toLowerCase().replace(/delete/, 'del')
switch method
when 'get', 'post', 'del', 'put'
r = request[method](
uri: process.env.PROXY_URL + req.url
json: req.body)
else
return res.send('invalid method')
req.pipe(r).pipe res
javascript:
app.use(function(req, res, next) {
var method, r;
method = req.method.toLowerCase().replace(/delete/,"del");
switch (method) {
case "get":
case "post":
case "del":
case "put":
r = request[method]({
uri: process.env.PROXY_URL + req.url,
json: req.body
});
break;
default:
return res.send("invalid method");
}
return req.pipe(r).pipe(res);
});
I found a shorter solution that does exactly what I want https://github.com/http-party/node-http-proxy
After installing http-proxy
npm install http-proxy --save
Use it like below in your server/index/app.js
var proxyServer = require('http-route-proxy');
app.use('/api/BLABLA/', proxyServer.connect({
to: 'other_domain.com:3000/BLABLA',
https: true,
route: ['/']
}));
I really have spent days looking everywhere to avoid this issue, tried plenty of solutions and none of them worked but this one.
Hope it is going to help someone else too :)
I don't have have an express sample, but one with plain http-proxy package. A very strip down version of the proxy I used for my blog.
In short, all nodejs http proxy packages work at the http protocol level, not tcp(socket) level. This is also true for express and all express middleware. None of them can do transparent proxy, nor NAT, which means keeping incoming traffic source IP in the packet sent to backend web server.
However, web server can pickup original IP from http x-forwarded headers and add it into the log.
The xfwd: true in proxyOption enable x-forward header feature for http-proxy.
const url = require('url');
const proxy = require('http-proxy');
proxyConfig = {
httpPort: 8888,
proxyOptions: {
target: {
host: 'example.com',
port: 80
},
xfwd: true // <--- This is what you are looking for.
}
};
function startProxy() {
proxy
.createServer(proxyConfig.proxyOptions)
.listen(proxyConfig.httpPort, '0.0.0.0');
}
startProxy();
Reference for X-Forwarded Header: https://en.wikipedia.org/wiki/X-Forwarded-For
Full version of my proxy: https://github.com/J-Siu/ghost-https-nodejs-proxy
I think you should use cors npm
const app = express();
const cors = require('cors');
var corsOptions = {
origin: 'http://localhost:3000',
optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204
}
app.use(cors(corsOptions));
https://www.npmjs.com/package/cors

How to install SSL for the following setup (React Frontend + Nodejs Backend + Custom Domain Heroku)

General information about my setup
Currently I am building a web application using react and a nodejs API that is providing the data for this web application. Both apps are hosted on heroku.com and run independently from each other. I have bought a custom domain from a different hosting provider and used the heroku custom domain option to point the DNS to my website.
Technical details about my setup
NodeJS server: Express
NodeJS version: v10.15.0
React version: v16.2.0
Custom domain: www.tabbs.nl
Heroku domain: tabbs-web-app.herokuapp.com
The issue I am experiencing
I have been digging into a lot of documentation and tutorials in order to setup SSL for react / NodeJS but couldn't find a decent tutorial about how to set SSL / security for my setup.
Tutorials I already have read:
Node + Express + Lets Encrypt
How to use SSL/TLS with nodejs
Stack overflow posts and probably a whole lot more I am forgetting right now.
What do I want to achieve?
The goal I would like to achieve is setting up a secure connection between React web application (frontend) and NodeJS API (backend) so that all data between those is encrypted and safe. Also I want my custom domain (bought by a different hosting provider than Heroku) to be secure and forced using https.
For any questions or additional information please do not hesitate to ask!
Have you tried using the https module in node?
You can do something like this:
var express = require('express');
var https = require('https');
var http = require('http');
var app = express();
http.createServer(app).listen(80);
https.createServer(options, app).listen(443);
The app returned by express() is in fact a JavaScript Function, designed to be passed to Node’s HTTP servers as a callback to handle requests. This makes it easy to provide both HTTP and HTTPS versions of your app with the same code base, as the app does not inherit from these (it is simply a callback.
If you are using create react app, open your terminal and type “npm run build”. This creates a build folder with all of your static files.
Now go back to your node backend service and add the following:
var express = require('express');
var path = require('path');
var https = require('https');
var http = require('http');
var app = express();
const options = {
key: fs.readFileSync("/srv/www/keys/my-site-key.pem"),
cert: fs.readFileSync("/srv/www/keys/chain.pem")
};
http.createServer(app).listen(80);
https.createServer(options, app).listen(443);
app.use(express.static(path.join(__dirname, 'build')));
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
If you’re using react router to handle routing for you web app then you would amend the GET request as such:
var express = require('express');
const path = require('path');
var https = require('https');
var http = require('http');
var app = express();
const options = {
key: fs.readFileSync("/srv/www/keys/my-site-key.pem"),
cert: fs.readFileSync("/srv/www/keys/chain.pem")
};
http.createServer(app).listen(80);
https.createServer(options, app).listen(443);
app.use(express.static(path.join(__dirname, 'build')));
app.get('/*', function(req, res) {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
This ain't a complex issue, do not worry about ssl, just create your own certificate for Node.JS/Express and that's enough.
and, React has a built-in way of doing api calls,
add this line to package.json of your React installation,
"proxy": "http://localhost:8000/"
and just call the api service like this,
//Generic API Call
callApi = async () => {
const response = await fetch('/api/hello');
const body = await response.json();
if (response.status !== 200) throw Error(body.message);
return body;
};
// A Submit handler to proxy
handleSubmit = async e => {
e.preventDefault();
const response = await fetch('/api/myrequest', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ post: this.state.post }),
});
const body = await response.text();
this.setState({ responseToPost: body });
};
it all works.

Express http 2 server push

I've made a small server to try http2 on node, however I can't tell if the push mechanism is working or not. My style is loaded over http2 but that does not mean that push is working as intended...
const port = 3000
const spdy = require('spdy')
const express = require('express')
const path = require('path')
const fs = require('fs')
const app = express()
app.get('*', (req, res) => {
res.set('Link', '</css/styles.css>; rel=preload; as=style');
res.sendFile(__dirname + '/index.html')
})
const options = {
key: fs.readFileSync(__dirname + '/keys/server.key'),
cert: fs.readFileSync(__dirname + '/keys/server.crt')
}
spdy.createServer(options, app).listen(3000);
In the devtools, the initiator says : "other".
You need to explicitly tell your express server which resources to push. Look here for a decent run through of it:
https://webapplog.com/http2-server-push-node-express/
An example route from that page:
app.get('/pushy', (req, res) => {
var stream = res.push('/main.js', {
status: 200, // optional
method: 'GET', // optional
request: {
accept: '*/*'
},
response: {
'content-type': 'application/javascript'
}
})
stream.on('error', function() {
})
stream.end('alert("hello from push stream!");')
res.end('<script src="/main.js"></script>')
})
Calling res.push() is the key here with the file you want to serve.
He also documents a way to write some middleware to handle pushing as well of assets:
https://webapplog.com/http2-server-push-express-middleware/
Once you have this implemented you can see the pushed items in Chrome devtools as in the answer from BazzaDP.
Chrome will show "Push / Other" in the Initiator column in Developer Tools->Network if the asset is pushed from the server:
I don't use the SDPY module in Node, but looks like from here and
here that this module doesn't use the Link header to push resources like some other servers (e.g. Apache) do. So unless you have, for example, Apache in front of this in HTTP/2 mode then I doubt your code will push the resource.

How to proxy to root path with node http-proxy

I am trying to setup a proxy with an express app to a root path from a specific path in my application:
http://my-domain.com/some/route --> http://another-domain:8000/
I have tried multiple things per the http-proxy docs but I keep hitting a wall with the paths/routing. I am trying to do this within a logged in express app so that I can utilize my authentication behind the app i'm trying to proxy too. I keep getting an error with the proxy'd app saying the path '/some/route' is not defined...etc.
var httpProxy = require('http-proxy');
var proxy = httpProxy.createProxyServer({});
proxy.proxyRequest(req, res, {
host:'localhost',
port:8000
});
I've also tried:
var url = 'http://localhost:8000/';
var httpProxy = require('http-proxy'),
proxy = httpProxy.createProxyServer({});
proxy.web(req,res, { target: url }, function(e) {
console.log('proxy.web callback');
console.log(e);
});
The function calls but I end up with an express 404 error...
I would also like to pass in some variables if that is possible so for example:
http://my-domain.com/some/route?var1=something&var2=something --> http://another-domain:8000/?var1=something&var2=something
But could not figure out if that was possible, I tried setting it on the request since that was being sent into the proxyRequest, but was unable to find them in the second application.
No, you can't do this with just node-http-proxy.
But it's possible with http-proxy-middleware (and you likely use it already):
From comment by #chimurai on github:
You can rewrite paths with the pathRewrite option.
var options = {
target: 'http://test.com',
changeOrigin: true,
pathRewrite: {'^/api' : ''} // <-- this will remove the /api prefix
};
server.middleware = proxyMiddleware('/api', options);
And if you come here because you're using webpack-dev-server, note that it also internally uses http-proxy-middleware, starting from version 2.0.0-beta (see PR).
Side note: There is also a node-proxy plugin, http-proxy-rules, so you can use this one if you don't want middleware.
Well, I encounter another problem, but needed to solve this problem first. I came up with this code, which worked fine for me ;)
Just use this for "/some/route"
.... // your stuff
httpProxy.on('error', function (err, req, res) {
res.writeHead(500, {
'Content-Type': 'text/plain'
});
res.end('some error');
});
app.all( '/some/route/*' , function( req , res ) {
var url = req.url;
url = url.slice(11); // to remove "/some/route"
req.url = url;
return httpProxy.web(req, res , { target: "http://another-domain:8000" } );
} );
hope this helps.

Node.js Http-proxy How to pass information to target server

I am building a web application using node.js and I need to proxy certain routes to another Node.js (express) server
I have the below code
var express = require('express'),
http=require('http'),
httpProxy = require('http-proxy'),
proxy = new httpProxy.RoutingProxy();
var server = express();
server.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
server.use(express.logger());
server.use(express.cookieParser());
server.use(express.query());
server.use(express.session({secret:"secret-key", maxAge:10*60*1000}));
server.use(routeRequest);
var routeRequest = function (req, res, next) {
req.url=req.url.substring(1);
if (req.url.indexOf("client")==0) {
proxyToClientServer(9001,req,res)
}else{
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end();
}
}
var proxyToClientServer=function(port, req, res){
req.headers["data_context"]='customer_details';
proxy.proxyRequest(req, res, {
host: 'localhost',
port: port,
headers:req.headers
})
}
server.listen(8000);
My problem is the request gets routed properly, but the data context (custom header) i added is removed from the request at the target server
Can somebody please advice how to pass more information to the server thats is being proxied
As robertklep mentioned, the code was working , I was looking at the wrong place.
But one thing to remember is that we could only add Strings. Any objects I added was gone but I had to JSON.stringify(obj) and add to header to make it go through.
Objects that could not be stringified (like socket handles) could not be passed along.

Resources