• No results found

Fork the Process, Catch Errors

In document Secure Your Node.js Web Application (Page 47-53)

You should now understand that once an unexpected error is thrown, the process is on its way to the junkyard. We will need to minimize the impact.

The best way is to have more than one process, or forking.

There are two main benefits to forking: it enables the application to make better use of system resources, and it supports encapsulation with a graceful restart of failed processes. From a security standpoint, we’re most interested in encapsulation.

The goal is to create the master process that would fork itself into multiple processes. When one of the child processes encounters an error, all the traffic to that process is stopped and a new fork is created, as the following graphic shows. The old fork is allowed to finish handling the requests in its queue before being terminated. It keeps reforking new processes, but the leaked references don’t accumulate. If service is interrupted, forking makes sure only a small number of users are affected.

This can be achieved by using either built-in Node.js modules or external solutions. For our examples, we’ll look at the built-in cluster module.8

We’ll start by first creating a process that’s forked into multiples. This is a basic example of a web server that’s forked as many times as there are CPUs:

chp-3-networking/cluster-simple.js 'use strict';

var cluster = require('cluster');

8. http://nodejs.org/api/cluster.html

Chapter 3. Start Connecting

36

var http = require('http');

// Ask the number of CPU-s for optimal forking (one fork per CPU) var numCPUs = require('os').cpus().length;

// Log when a worker exits

cluster.on('exit', function(worker, code, signal) { console.log('worker ' + worker.process.pid + ' died');

});

} else {

// Workers can share any TCP connection // In this case its a HTTP server http.createServer(function(req, res) {

res.writeHead(200);

res.end("hello world\n");

}).listen(3000);

}

But this example does nothing except log when a worker dies. If we add a fork restart and a generic error handler for the fork to gracefully terminate the process, we’ll improve security. The next code example is long, because it also introduces domains to keep the ability to respond to your failed request:

chp-3-networking/cluster-domains.js 'use strict';

var cluster = require('cluster');

// Ask the number of CPU-s for optimal forking (one fork per CPU) var numCPUs = require('os').cpus().length;

var PORT = +process.env.PORT || 3000;

if (cluster.isMaster) {

// In real life, you'd probably not put the master and worker in the same file.

//

// You can also of course get a bit fancier about logging, and // implement whatever custom logic you need to prevent DoS // attacks and other bad behavior.

//

// See the options in the cluster documentation.

//

// The important thing is that the master does very little, // increasing our resilience to unexpected errors.

// Fork workers.

for (var i = 0; i < numCPUs; i++) {

Don’t Forget About Proper Error Handling

37

cluster.fork();

}

cluster.on('disconnect', function(worker) { console.error('disconnect!');

cluster.fork();

});

} else {

// the worker //

// This is where we put our bugs!

var domain = require('domain');

// See the cluster documentation for more details about using // worker processes to serve requests. How it works, caveats, etc.

var server = require('http').createServer(function(req, res) { var d = domain.create();

d.on('error', function(er) {

console.error('error', er.stack);

// Note: we're in dangerous territory!

// By definition, something unexpected occurred, // which we probably didn't want.

// Anything can happen now! Be very careful!

try {

// make sure we close down within 30 seconds var killtimer = setTimeout(function() {

process.exit(1);

}, 30000);

// But don't keep the process open just for that!

killtimer.unref();

// stop taking new requests.

server.close();

// Let the master know we're dead. This will trigger a // 'disconnect' in the cluster master, and then it will fork // a new worker.

cluster.worker.disconnect();

// try to send an error to the request that triggered the problem res.statusCode = 500;

res.setHeader('content-type', 'text/plain');

res.end('Oops, there was a problem!\n');

} catch (er2) {

// oh well, not much we can do at this point.

Chapter 3. Start Connecting

38

console.error('Error sending 500!', er2.stack);

} });

// Because req and res were created before this domain existed, // we need to explicitly add them.

d.add(req);

d.add(res);

// Now run the handler function in the domain.

d.run(function() {

// This part isn't important. Just an example routing thing.

// You'd put your fancy application logic here.

function handleRequest(req, res) { switch(req.url) {

case '/error':

// We do some async stuff, and then...

setTimeout(function() { refork. However, keeping the clustering code in the same file as the application is cumbersome and ugly, something we don’t want to do with real-life appli-cations. So let’s use a separate file, as shown in this example:

chp-3-networking/cluster-separate-file.js 'use strict';

var cluster = require('cluster');

// Ask the number of CPU-s for optimal forking (one fork per CPU) var numCPUs = require('os').cpus().length;

cluster.setupMaster({

exec : __dirname + '/index.js' // Points to the index file you want to fork });

// Fork workers.

Don’t Forget About Proper Error Handling

39

for (var i = 0; i < numCPUs; i++) { cluster.fork();

}

cluster.on('disconnect', function(worker) {

console.error('disconnect!'); // This can probably use some work.

cluster.fork();

});

This code should be in master.js with the main server code in index.js, which needs only slight—or no—alternations to be able to fork or gracefully restart processes. We now have a graceful way to handle errors, and our application uses system resources more efficiently. Better performance and improved security—what a win!

Forking the process as many times as there are CPUs can, however, clog the processor. Unfortunately, there’s no golden rule to follow when deciding the number of forks you should have. The number is determined by the structure and infrastructure of the application. If you have other vital processes on the machine, such as the database, then some of the resources are already in use, and you need to account for that before you start forking.

After all this, there’s always the possibility the process might exit unexpect-edly. It could be because some other process consumed all available memory, for example. The process must somehow be restarted, or the website will be unavailable until the administrator has time to restart everything. That’s not an ideal situation to be in.

We have lots of different ways to do this depending on the operating system and tools. Under Unix, we can just daemonize our node script. But there are also Node.js modules for this purpose that are operating-system agnostic.

Two popular choices are forever,9 which has been around for a long while, and PM2.10 While forever is lightweight, PM2 is a feature-rich alternative, with auto-matic clusterization, load balancing, log aggregation, and other features. It’s definitely worth checking out.

Wrapping Up

Fabulous—in this chapter you learned about various ways you can secure your networking layer and keep your service running despite errors (that others have somehow sneaked into your code). We covered how encryption protects messages so that others can’t listen in, how logging helps you

9. https://github.com/nodejitsu/forever 10. https://github.com/Unitech/pm2

Chapter 3. Start Connecting

40

understand and defend your application, and how to use error handling to prevent application crashes.

Let’s now move on to the most popular attack vector out there: code injection.

In the next chapter, we’ll look at different code injection attacks and how to prevent them.

Wrapping Up

41

CHAPTER 4

To expect the unexpected shows a thoroughly modern intellect.

➤ Oscar Wilde

In document Secure Your Node.js Web Application (Page 47-53)