Friday, September 30, 2011

A nodejs server to run shell commands

WARNING: Since I initially posted about this the JDK patched the issue and there is no need to use a server like this to run commands from a JVM.

I was tempted to use jetty as a lightweight server to resolve the JVM-fork() memory problem as I already posted however nodejs provides a lighter and so from my simplistic design poit of view better alternative.

Below is the code for such a server:
#!/usr/local/bin/node
/*
** shell-server.js returns json response with the stdout and stderr of a shell command
**
**
** @Author: Nestor Urquiza
** @Date: 09/29/2011
**
*/

/*
* Dependencies
*/
var http = require('http'),
    url = require('url'),
    exec = require('child_process').exec;

/*
* Server Config
*/
var host = "127.0.0.1",
    port = "8088",
    thisServerUrl = "http://" + host + ":" + port;

/*
* Main
*/
http.createServer(function (req, res) {
  req.addListener('end', function () {
        
  });
  var parsedUrl = url.parse(req.url, true);
  var cmd = parsedUrl.query['cmd'];
  var async = parsedUrl.query['async'];

  res.writeHead(200, {'Content-Type': 'text/plain'});

  if( cmd ) {
    var child = exec(cmd, function (error, stdout, stderr) {
      var result = '{"stdout":' + stdout + ',"stderr":"' + stderr + '","cmd":"' + cmd + '"}';
      res.end(result + '\n');
    });
  } else {
    var result = '{"stdout":"' + '' + '","stderr":"' + 'cmd is mandatory' + '","cmd":"' + cmd + '"}';
    res.end(result + '\n');
  }  
  if(async == "true") {
    var result = '{"stdout":"async request' + '' + '","stderr":"' + '' + '","cmd":"' + cmd + '"}';
    res.end(result + '\n');
  }

}).listen(port, host);
console.log('Server running at ' + thisServerUrl );

Once the server is running you can hit the below URL to get a list of the users loged in a OSX/linux/unix box:
http://localhost:8088/?cmd=who
Here is how to run a sleep command and a touch command demonstrating the usage of async=true which basically will run the commands but will not check for stdout nor stderr. You can see the response comes back instantaneously however the file will be touched 5 seconds later:
$ ls --full-time /tmp/here
-rw-r--r-- 1 dev dev 0 2014-03-06 16:17:07.546398632 -0500 /tmp/here
$ date
Thu Mar  6 16:17:08 EST 2014
$ curl "http://localhost:8088/?cmd=sleep%2010;%20touch%20/tmp/here&async=true"
{"stdout":"async request","stderr":"","cmd":"sleep 10; touch /tmp/here"}
$ date
Thu Mar  6 16:17:12 EST 2014
$ ls --full-time /tmp/here
-rw-r--r-- 1 dev dev 0 2014-03-06 16:17:20.602399455 -0500 /tmp/here

Development environment

If you are running Windows you should download the node executable and run the server as:
node c:\shell-server.js

If you are running OSX/Linux/Unix you have to install node make the script executable and run the below:
/path/to/shell-server.sh

Production deployment

You need to be sure the server runs as a daemon and that it restarts if it fails to serve. In debian/Ubuntu + monit you can follow these steps:
$ sudo vi /opt/nodejs/shell-server.js
$ sudo vi /etc/init/shell-server.conf
#!/sbin/upstart
description "shell server runs a command specified by HTTP GET param 'cmd'"
author      "admin"

start on startup
stop on shutdown

script
    #export HOME="/root"
    #exec sudo -u admin /usr/local/bin/node /opt/nodejs/shell-server.js
        #exec su -c "/usr/local/bin/node /opt/nodejs/shell-server.js" dev
    exec start-stop-daemon --start -c dev --exec /usr/local/bin/node /opt/nodejs/shell-server.js
end script
$ sudo vi /etc/monit/monitrc 
...
#################################################################
# shell-server
################################################################

check host shell-server with address 127.0.0.1
start = "/sbin/start shell-server"
stop = "/sbin/stop shell-server"
if failed port 8088 protocol HTTP
  request /
  with timeout 10 seconds
then restart
group server
...
$ sudo monit reload
$ sudo vi /opt/nodejs/shell-server.js
$ wget http://localhost:8088 -O -
Note that upstart will log stdout and stderr to /var/log/upstart/shell-server.log

7 comments:

Kenan said...

?cmd=cat /etc/passwd
?cmd=passwd //what happens?
?cmd=curl |sh
?cmd=rm -rf * ../ ../../

Nestor Urquiza said...

@Kenan it will happen the same as if you write e thise commands from Runtime.exec(). Security should be handled of course but that is out of tge scope of this post which is just about running commamds from Java without paying a high toll in memory consumption.

Unknown said...

Very good, could you use crypto to encrypt and decrypt the commands?

Nestor Urquiza said...

@Ross the main motivation for this solution was to locally execute commands. Definitely support for SSL is never a bad idea even if running local commands so I would definitely vote for using crypto to support HTTPS instead of plain HTTP.

Unknown said...
This comment has been removed by the author.
Unknown said...

Great!! simple && powerful && controlrable!!

bitfinfo said...

Instead of exec, you could also exec the commands over the same background process(es) for stateful behavior; see https://github.com/bitsofinfo/stateful-process-command-proxy

Followers