master
Raw Download raw file
  1#!/usr/bin/env node
  2
  3"use strict";
  4
  5var crypto = require('crypto');
  6var fs = require('fs');
  7var http = require("http");
  8var nopt = require('nopt');
  9var url = require("url");
 10
 11var util = require('./lib/util');
 12
 13function respond(response, statusCode, contents) {
 14  response.writeHead(statusCode, {"Content-Type": "application/json"});
 15  if (contents) {
 16    response.write(JSON.stringify(contents));
 17  }
 18  response.end();
 19}
 20
 21function respondError(response, responseCode, errorMessage) {
 22  var contents = {
 23    "error": errorMessage
 24  };
 25  respond(response, responseCode, contents);
 26}
 27
 28var DownTime = function () {
 29  this.startTime = Date.now();
 30};
 31DownTime.prototype.end = function () {
 32  this.endTime = Date.now();
 33};
 34DownTime.prototype.duration = function () {
 35  var endTime = this.endTime || Date.now()
 36  return (endTime - this.startTime);
 37};
 38
 39var QueueServer = function (responseDelay, allowedInFlight, maxQueueLength) {
 40  this.responseDelay = responseDelay;
 41  this.allowedInFlight = allowedInFlight;
 42  this.maxQueueLength = maxQueueLength;
 43  this.inFlight = 0;
 44  this.queued = [];
 45  this.downtimes = [];
 46};
 47QueueServer.prototype.pushRequest = function (reqData) {
 48  if (this.queued.length >= this.maxQueueLength) {
 49    respondError(reqData.response, "500", "Server is falling over from the load.");
 50  } else {
 51    this.queued.push(reqData);
 52  }
 53};
 54QueueServer.prototype.popRequest = function () {
 55  if (this.queued.length === 0) {
 56    this.inFlight -= 1;
 57    this.downtimes.push(new DownTime());
 58    return;
 59  }
 60  var value = this.queued[0];
 61  this.queued.splice(0, 1);
 62  this.handleRequest(value);
 63};
 64QueueServer.prototype.handleRequest = function (reqData) {
 65  if (reqData.closed) {
 66    process.nextTick(this.popRequest.bind(this));
 67    return;
 68  }
 69  var parsed = url.parse(reqData.request.url, true),
 70      nonce = parsed.query.nonce,
 71      contents,
 72      hmac;
 73  if (nonce === undefined) {
 74    respondError(reqData.response, "400", "No nonce was provided.");
 75    process.nextTick(this.popRequest.bind(this));
 76    return;
 77  }
 78  hmac = util.sign(nonce, this.secret);
 79  contents = {
 80    "hmac": hmac
 81  };
 82  setTimeout(this.waitAndRespond.bind(this), this.responseDelay, reqData, contents);
 83};
 84QueueServer.prototype.waitAndRespond = function (reqData, contents) {
 85  if (reqData.closed) {
 86    console.log("Client timed out while waiting for us.");
 87  } else {
 88    respond(reqData.response, 200, contents);
 89  }
 90  this.popRequest();
 91};
 92
 93var RequestData = function (request, response) {
 94  this.request = request;
 95  this.response = response;
 96  this.closed = false;
 97};
 98
 99function handleGET(queue, request, response) {
100  var reqData = new RequestData(request, response);
101  response.on('close', function () {
102    reqData.closed = true;
103  });
104  if (queue.inFlight >= queue.allowedInFlight) {
105    // Save this request for later
106    queue.pushRequest(reqData);
107  } else {
108    // Handle now
109    if (queue.downtimes.length != 0) {
110      queue.downtimes[queue.downtimes.length - 1].end();
111    }
112    queue.inFlight += 1;
113    queue.handleRequest(reqData);
114  }
115}
116
117function handleHEAD(validPath, request, response) {
118  var parsed = url.parse(request.url);
119  if (parsed['pathname'] == "/" + validPath) {
120    respond(response, 200);
121  } else {
122    respond(response, 400);
123  }
124}
125
126function main() {
127  var opts = {
128    "secret": String,
129    "in-socket": String,
130    "in-port": String,
131  };
132  var parsed = nopt(opts),
133      secret = parsed.secret || "defaultsecret",
134      secretHash = util.hash(secret),
135      listenTarget;
136
137  if (parsed['in-socket'] !== undefined && parsed['in-port'] !== undefined) {
138    console.log("Cannot specify both an in-port and an in-socket. Exiting.");
139    process.exit(1);
140  } else if (parsed['in-socket']) {
141    listenTarget = parsed['in-socket'];
142  } else {
143    // Default: listen on port 3001
144    listenTarget = parsed['in-port'] || '3001';
145  }
146  // Response delay in ms, Allowed in flight connections, Allowed queue length
147  var queue = new QueueServer(75, 2, 4);
148  queue.secret = secret;
149  var server = http.createServer(function (request, response) {
150    switch (request.method) {
151    case "GET":
152      handleGET(queue, request, response);
153      break;
154    case "HEAD":
155      handleHEAD(secretHash, request, response);
156      break;
157    default:
158      respondUserError(response, "Unsupported HTTP method " + request.method);
159    }
160  });
161
162  server.on("listening", function () {
163    if (parsed['in-socket'] !== undefined) {
164      fs.chmodSync(parsed['in-socket'], "0666");
165    }
166  })
167
168  console.log("The backend is up and listening.");
169  server.listen(listenTarget);
170}
171
172main();