// Copyright 2012 Joyent, Inc. All rights reserved.

var assert = require('assert-plus'); var util = require('util'); var utils = require('./utils');

///— Globals

var HASH_ALGOS = utils.HASH_ALGOS; var PK_ALGOS = utils.PK_ALGOS; var HttpSignatureError = utils.HttpSignatureError; var InvalidAlgorithmError = utils.InvalidAlgorithmError; var validateAlgorithm = utils.validateAlgorithm;

var State = {

New: 0,
Params: 1

};

var ParamsState = {

Name: 0,
Quote: 1,
Value: 2,
Comma: 3

};

///— Specific Errors

function ExpiredRequestError(message) {

HttpSignatureError.call(this, message, ExpiredRequestError);

} util.inherits(ExpiredRequestError, HttpSignatureError);

function InvalidHeaderError(message) {

HttpSignatureError.call(this, message, InvalidHeaderError);

} util.inherits(InvalidHeaderError, HttpSignatureError);

function InvalidParamsError(message) {

HttpSignatureError.call(this, message, InvalidParamsError);

} util.inherits(InvalidParamsError, HttpSignatureError);

function MissingHeaderError(message) {

HttpSignatureError.call(this, message, MissingHeaderError);

} util.inherits(MissingHeaderError, HttpSignatureError);

function StrictParsingError(message) {

HttpSignatureError.call(this, message, StrictParsingError);

} util.inherits(StrictParsingError, HttpSignatureError);

///— Exported API

module.exports = {

/**
 * Parses the 'Authorization' header out of an http.ServerRequest object.
 *
 * Note that this API will fully validate the Authorization header, and throw
 * on any error.  It will not however check the signature, or the keyId format
 * as those are specific to your environment.  You can use the options object
 * to pass in extra constraints.
 *
 * As a response object you can expect this:
 *
 *     {
 *       "scheme": "Signature",
 *       "params": {
 *         "keyId": "foo",
 *         "algorithm": "rsa-sha256",
 *         "headers": [
 *           "date" or "x-date",
 *           "digest"
 *         ],
 *         "signature": "base64"
 *       },
 *       "signingString": "ready to be passed to crypto.verify()"
 *     }
 *
 * @param {Object} request an http.ServerRequest.
 * @param {Object} options an optional options object with:
 *                   - clockSkew: allowed clock skew in seconds (default 300).
 *                   - headers: required header names (def: date or x-date)
 *                   - algorithms: algorithms to support (default: all).
 *                   - strict: should enforce latest spec parsing
 *                             (default: false).
 * @return {Object} parsed out object (see above).
 * @throws {TypeError} on invalid input.
 * @throws {InvalidHeaderError} on an invalid Authorization header error.
 * @throws {InvalidParamsError} if the params in the scheme are invalid.
 * @throws {MissingHeaderError} if the params indicate a header not present,
 *                              either in the request headers from the params,
 *                              or not in the params from a required header
 *                              in options.
 * @throws {StrictParsingError} if old attributes are used in strict parsing
 *                              mode.
 * @throws {ExpiredRequestError} if the value of date or x-date exceeds skew.
 */
parseRequest: function parseRequest(request, options) {
  assert.object(request, 'request');
  assert.object(request.headers, 'request.headers');
  if (options === undefined) {
    options = {};
  }
  if (options.headers === undefined) {
    options.headers = [request.headers['x-date'] ? 'x-date' : 'date'];
  }
  assert.object(options, 'options');
  assert.arrayOfString(options.headers, 'options.headers');
  assert.optionalNumber(options.clockSkew, 'options.clockSkew');

  if (!request.headers.authorization)
    throw new MissingHeaderError('no authorization header present in ' +
                                 'the request');

  options.clockSkew = options.clockSkew || 300;

  var i = 0;
  var state = State.New;
  var substate = ParamsState.Name;
  var tmpName = '';
  var tmpValue = '';

  var parsed = {
    scheme: '',
    params: {},
    signingString: '',

    get algorithm() {
      return this.params.algorithm.toUpperCase();
    },

    get keyId() {
      return this.params.keyId;
    }
  };

  var authz = request.headers.authorization;
  for (i = 0; i < authz.length; i++) {
    var c = authz.charAt(i);

    switch (Number(state)) {

    case State.New:
      if (c !== ' ') parsed.scheme += c;
      else state = State.Params;
      break;

    case State.Params:
      switch (Number(substate)) {

      case ParamsState.Name:
        var code = c.charCodeAt(0);
        // restricted name of A-Z / a-z
        if ((code >= 0x41 && code <= 0x5a) || // A-Z
            (code >= 0x61 && code <= 0x7a)) { // a-z
          tmpName += c;
        } else if (c === '=') {
          if (tmpName.length === 0)
            throw new InvalidHeaderError('bad param format');
          substate = ParamsState.Quote;
        } else {
          throw new InvalidHeaderError('bad param format');
        }
        break;

      case ParamsState.Quote:
        if (c === '"') {
          tmpValue = '';
          substate = ParamsState.Value;
        } else {
          throw new InvalidHeaderError('bad param format');
        }
        break;

      case ParamsState.Value:
        if (c === '"') {
          parsed.params[tmpName] = tmpValue;
          substate = ParamsState.Comma;
        } else {
          tmpValue += c;
        }
        break;

      case ParamsState.Comma:
        if (c === ',') {
          tmpName = '';
          substate = ParamsState.Name;
        } else {
          throw new InvalidHeaderError('bad param format');
        }
        break;

      default:
        throw new Error('Invalid substate');
      }
      break;

    default:
      throw new Error('Invalid substate');
    }

  }

  if (!parsed.params.headers || parsed.params.headers === '') {
    if (request.headers['x-date']) {
      parsed.params.headers = ['x-date'];
    } else {
      parsed.params.headers = ['date'];
    }
  } else {
    parsed.params.headers = parsed.params.headers.split(' ');
  }

  // Minimally validate the parsed object
  if (!parsed.scheme || parsed.scheme !== 'Signature')
    throw new InvalidHeaderError('scheme was not "Signature"');

  if (!parsed.params.keyId)
    throw new InvalidHeaderError('keyId was not specified');

  if (!parsed.params.algorithm)
    throw new InvalidHeaderError('algorithm was not specified');

  if (!parsed.params.signature)
    throw new InvalidHeaderError('signature was not specified');

  // Check the algorithm against the official list
  parsed.params.algorithm = parsed.params.algorithm.toLowerCase();
  try {
    validateAlgorithm(parsed.params.algorithm);
  } catch (e) {
    if (e instanceof InvalidAlgorithmError)
      throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' +
        'supported'));
    else
      throw (e);
  }

  // Build the signingString
  for (i = 0; i < parsed.params.headers.length; i++) {
    var h = parsed.params.headers[i].toLowerCase();
    parsed.params.headers[i] = h;

    if (h === 'request-line') {
      if (!options.strict) {
        /*
         * We allow headers from the older spec drafts if strict parsing isn't
         * specified in options.
         */
        parsed.signingString +=
          request.method + ' ' + request.url + ' HTTP/' + request.httpVersion;
      } else {
        /* Strict parsing doesn't allow older draft headers. */
        throw (new StrictParsingError('request-line is not a valid header ' +
          'with strict parsing enabled.'));
      }
    } else if (h === '(request-target)') {
      parsed.signingString +=
        '(request-target): ' + request.method.toLowerCase() + ' ' +
        request.url;
    } else {
      var value = request.headers[h];
      if (value === undefined)
        throw new MissingHeaderError(h + ' was not in the request');
      parsed.signingString += h + ': ' + value;
    }

    if ((i + 1) < parsed.params.headers.length)
      parsed.signingString += '\n';
  }

  // Check against the constraints
  var date;
  if (request.headers.date || request.headers['x-date']) {
      if (request.headers['x-date']) {
        date = new Date(request.headers['x-date']);
      } else {
        date = new Date(request.headers.date);
      }
    var now = new Date();
    var skew = Math.abs(now.getTime() - date.getTime());

    if (skew > options.clockSkew * 1000) {
      throw new ExpiredRequestError('clock skew of ' +
                                    (skew / 1000) +
                                    's was greater than ' +
                                    options.clockSkew + 's');
    }
  }

  options.headers.forEach(function (hdr) {
    // Remember that we already checked any headers in the params
    // were in the request, so if this passes we're good.
    if (parsed.params.headers.indexOf(hdr) < 0)
      throw new MissingHeaderError(hdr + ' was not a signed header');
  });

  if (options.algorithms) {
    if (options.algorithms.indexOf(parsed.params.algorithm) === -1)
      throw new InvalidParamsError(parsed.params.algorithm +
                                   ' is not a supported algorithm');
  }

  return parsed;
}

};