steve-jansen/json-proxy

View on GitHub
lib/config.js

Summary

Maintainability
A
3 hrs
Test Coverage
'use strict';

var fs      = require('fs'),
    path    = require('path'),
    url     = require('url');

module.exports = function configParser(options) {
  var config = createDefaultConfig();

  options = options || {};

  // read environmental variables first
  config = parseEnv(config);

  // read a config file next
  config = parseFile(options.file, config);

  // process any command line arguments
  config = parseCommandLine(options.argv, config);

  // process the hard code values
  parseConfig(options, config);

  // normalize the path to config.server.webroot
  if (config.server.webroot && config.server.webroot.length > 0) {
    config.server.webroot = path.normalize(config.server.webroot);
  }

  return config;
};

function createDefaultConfig() {
  return {
    server: {
      port: 8080,
      webroot: process.cwd(),
      html5mode: false
    },
    proxy: {
      gateway: null,
      forward: [],
      headers: []
    }
  };
}

function parseEnv(config) {
  var env = process.env;
  var temp;

  if (env['JSON_PROXY_PORT']) {
    temp = parseInt(env['JSON_PROXY_PORT'], 10);
    if (temp && !isNaN(temp)) {
      config.server.port = temp;
    }
  }

  if (env['JSON_PROXY_WEBROOT']) {
    temp = path.normalize(env['JSON_PROXY_WEBROOT']);
    if (temp && fs.existsSync(temp)) {
      config.server.webroot = temp;
    }
  }

  if (env['JSON_PROXY_GATEWAY']) {
    config.proxy.gateway = parseGateway(env['JSON_PROXY_GATEWAY'], env['JSON_PROXY_GATEWAY_AUTH']);
  }

  return config;
}

// reads a config file from either the config file specified on the command line, 
// or fallback to a file name json-proxy.config in the working directory
// return true if the file can be read, otherwise return false
function parseFile(filepath, config) {
  var contents;

  filepath = filepath || path.join(process.cwd(), '/json-proxy.json');

  // if we were passed a config file, read and parse it
  if (fs.existsSync(filepath)) {
    try {
      var data = fs.readFileSync(filepath);
      contents = JSON.parse(data.toString());
      config = parseConfig(contents, config);

      // replace the token $config_dir in the webroot arg
      if (config.server.webroot && config.server.webroot.length > 0) {
        config.server.webroot = config.server.webroot.replace("$config_dir", path.dirname(filepath));
      }
    } catch (ex) {
      throw new Error('Cannot parse the config file "' + filepath + '": ' + ex);
    }

  }

  return config;
}

// parse a config structure, overriding any values in config
function parseConfig(contents, config) {
  contents.server = contents.server || {};
  contents.proxy = contents.proxy || {};

  if (contents.proxy.gateway && typeof(contents.proxy.gateway) === "string" && contents.proxy.gateway.length > 0) {
    contents.proxy.gateway = parseGateway(contents.proxy.gateway);
  }

  contents.proxy.forward = parseConfigMap(contents.proxy.forward, parseForwardRule);
  contents.proxy.headers = parseConfigMap(contents.proxy.headers, parseHeaderRule);

  // override any values in the config object with values specified in the file;
  config.server.port = contents.server.port || config.server.port;
  config.server.webroot = contents.server.webroot || config.server.webroot;
  config.server.html5mode = contents.server.html5mode || config.server.html5mode;
  config.proxy.gateway = contents.proxy.gateway || config.proxy.gateway;
  config.proxy.forward = contents.proxy.forward || config.proxy.forward;
  config.proxy.headers = contents.proxy.headers || config.proxy.headers;

  return config;
}

// transform a config hash object into an array
function parseConfigMap(map, callback) {
  var result = [];

  if (!(map instanceof Object)) {
    return map;
  }

  for(var property in map) {
    if (map.hasOwnProperty(property)) {
      result.push(callback(property, map[property]));
    }
  }

  return result;
}

// reads command line parameters
function parseCommandLine(argv, config) {
  if (argv) {
    // read the command line arguments if no config file was given
    parseCommandLineArgument(argv.port, function(item){
      config.server.port = item;
    });

    parseCommandLineArgument(argv.html5mode, function(item){
      config.server.html5mode = item;
    });

    parseCommandLineArgument(argv._, function(item){
      config.server.webroot = path.normalize(item);
    });

    parseCommandLineArgument(argv.gateway, function(item){
      config.proxy.gateway = parseGateway(item);
    });

    parseCommandLineArgument(argv.forward, function(item){
      var rule = parseForwardRule(item);
      var match = false;

      config.proxy.forward.forEach(function(item) {
        if (item.regexp.source === rule.regexp.source) {
          item.target = rule.target;
          match = true;
        }
      });

      if (!match) {
        config.proxy.forward.push(rule);
      }
    });

    parseCommandLineArgument(argv.header, function(item){
      var rule = parseHeaderRule(item);
      var match = false;

      config.proxy.headers.forEach(function(item) {
        if (item.name === rule.name) {
          item.value = rule.value;
          match = true;
        }
      });

      if (!match) {
        config.proxy.headers.push(rule);
      }
    });
  }

  return config;
}

// argv.X will be an array if multiple -X options are provided
// otherwise argv.X will just be a scalar value
function parseCommandLineArgument(arg, fn) {
  if (typeof(fn) !== 'function')
    return;

  if (Array.isArray(arg)) {
    arg.forEach(function(item) {
      fn.call(null, item);
    });
  } else {
    if (arg !== null && arg !== undefined) {
      fn.call(null, arg);
    }
  }
}

function parseGateway(gateway, auth) {
  var config = null;

  if (gateway !== undefined && gateway !== null && typeof(gateway) === 'string' && gateway.length > 0) {
    config = parseTargetServer(gateway);
    config.auth = url.parse(gateway).auth;

    if (auth && typeof(auth) === "string" && auth.length > 0) {
      config.auth  = auth;
    }
  }

  return config;
}

function parseHeaderRule() {
  return tokenize.apply(null, arguments);
}

// parses rule syntax to create forwarding rules
function parseForwardRule() {
  var token,
      rule;

  if (arguments[0] === undefined || arguments[0] === null) {
    return;
  }

  if (typeof(arguments[0]) === "object") {
    return arguments[0];
  }

  try {
    token = tokenize.apply(null, arguments);
    rule = { regexp: new RegExp('^' + token.name, 'i'), target: parseTargetServer(token.value) };
  } catch (e) {
    throw new Error('cannot parse the forwarding rule ' + arguments[0] + ' - ' + e);
  }

  return rule;
}

// parses a simple hostname:port argument, defaulting to port 80 if not specified
function parseTargetServer(value) {
  var target,
      path;

  // insert a http protocol handler if not found in the string
  if (value.indexOf('http://') !== 0 && value.indexOf('https://') !== 0) {
    value = 'http://' + value + '/';
  }
  target = url.parse(value);
  path = target.path;

  // url.parse() will default to '/' as the path
  // we can safely strip this for regexp matches to function properly
  if (path === '/') {
    path = '';
  }

  // support an explict set of regexp unnamed captures (prefixed with `$`)
  // if the pattern doesn't include a match operator like '$1', '$2', '$3',
  // then use the RegExp lastMatch operator `$&` if the rewrite rule
  if (/\$\d/.test(path) === false) {
    path = [path ,'$&'].join('');
  }

  return {
    host: target.hostname,
    // inject a default port if one wasn't specified
    port: target.port || ((target.protocol === 'https:') ? 443 : 80),
    originalPort: target.port,
    protocol: target.protocol,
    path: path
  };
}

// reads name/value tokens for command line parameters
function tokenize() {
  var token = { name: null, value: null },
      temp = null;

  if (arguments.length !== 1) {
    token.name = arguments[0];
    token.value = arguments[1];
    return token;
  }

  temp = arguments[0];

  if (undefined !== temp && null !== temp) {
    temp = temp.split('=');
  }

  if (Array.isArray(temp) && temp.length > 1) {
    token.name = temp[0];
    token.value = temp[1];
  }

  return token;
}