text: var AWS = require('../core');
  var inherit = AWS.util.inherit;
  
  /**
   * @api private
   */
  var cachedSecret = {};
  
  /**
   * @api private
   */
  AWS.Signers.V4 = inherit(AWS.Signers.RequestSigner, {
    constructor: function V4(request, serviceName) {
      AWS.Signers.RequestSigner.call(this, request);
      this.serviceName = serviceName;
    },
  
    addAuthorization: function addAuthorization(credentials, date) {
      var datetime = AWS.util.date.iso8601(date).replace(/[:\-]|\.\d{3}/g, '');
      this.addHeaders(credentials, datetime);
      this.updateBody(credentials);
      this.request.headers['Authorization'] =
        this.authorization(credentials, datetime);
    },
  
    addHeaders: function addHeaders(credentials, datetime) {
      this.request.headers['X-Amz-Date'] = datetime;
      if (credentials.sessionToken) {
        this.request.headers['x-amz-security-token'] = credentials.sessionToken;
      }
    },
  
    updateBody: function updateBody(credentials) {
      if (this.request.params) {
        this.request.params.AWSAccessKeyId = credentials.accessKeyId;
  
        if (credentials.sessionToken) {
          this.request.params.SecurityToken = credentials.sessionToken;
        }
  
        this.request.body = AWS.util.queryParamsToString(this.request.params);
        this.request.headers['Content-Length'] = this.request.body.length;
      }
    },
  
    authorization: function authorization(credentials, datetime) {
      var parts = [];
      var credString = this.credentialString(datetime);
      parts.push('AWS4-HMAC-SHA256 Credential=' +
        credentials.accessKeyId + '/' + credString);
      parts.push('SignedHeaders=' + this.signedHeaders());
      parts.push('Signature=' + this.signature(credentials, datetime));
      return parts.join(', ');
    },
  
    signature: function signature(credentials, datetime) {
      var cache = cachedSecret[this.serviceName];
      var date = datetime.substr(0, 8);
      if (!cache ||
          cache.akid !== credentials.accessKeyId ||
          cache.region !== this.request.region ||
          cache.date !== date) {
        var kSecret = credentials.secretAccessKey;
        var kDate = AWS.util.crypto.hmac('AWS4' + kSecret, date, 'buffer');
        var kRegion = AWS.util.crypto.hmac(kDate, this.request.region, 'buffer');
        var kService = AWS.util.crypto.hmac(kRegion, this.serviceName, 'buffer');
        var kCredentials = AWS.util.crypto.hmac(kService, 'aws4_request', 'buffer');
        cachedSecret[this.serviceName] = {
          region: this.request.region, date: date,
          key: kCredentials, akid: credentials.accessKeyId
        };
      }
  
      var key = cachedSecret[this.serviceName].key;
      return AWS.util.crypto.hmac(key, this.stringToSign(datetime), 'hex');
    },
  
    stringToSign: function stringToSign(datetime) {
      var parts = [];
      parts.push('AWS4-HMAC-SHA256');
      parts.push(datetime);
      parts.push(this.credentialString(datetime));
      parts.push(this.hexEncodedHash(this.canonicalString()));
      return parts.join('\n');
    },
  
    canonicalString: function canonicalString() {
      var parts = [];
      parts.push(this.request.method);
      parts.push(this.request.pathname());
      parts.push(this.request.search());
      parts.push(this.canonicalHeaders() + '\n');
      parts.push(this.signedHeaders());
      parts.push(this.hexEncodedBodyHash());
      return parts.join('\n');
    },
  
    canonicalHeaders: function canonicalHeaders() {
      var headers = [];
      AWS.util.each.call(this, this.request.headers, function (key, item) {
        headers.push([key, item]);
      });
      headers.sort(function (a, b) {
        return a[0].toLowerCase() < b[0].toLowerCase() ? -1 : 1;
      });
      var parts = [];
      AWS.util.arrayEach.call(this, headers, function (item) {
        var key = item[0].toLowerCase();
        if (this.isSignableHeader(key)) {
          parts.push(key + ':' +
            this.canonicalHeaderValues(item[1].toString()));
        }
      });
      return parts.join('\n');
    },
  
    canonicalHeaderValues: function canonicalHeaderValues(values) {
      return values.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
    },
  
    signedHeaders: function signedHeaders() {
      var keys = [];
      AWS.util.each.call(this, this.request.headers, function (key) {
        key = key.toLowerCase();
        if (this.isSignableHeader(key)) keys.push(key);
      });
      return keys.sort().join(';');
    },
  
    credentialString: function credentialString(datetime) {
      var parts = [];
      parts.push(datetime.substr(0, 8));
      parts.push(this.request.region);
      parts.push(this.serviceName);
      parts.push('aws4_request');
      return parts.join('/');
    },
  
    hexEncodedHash: function hash(string) {
      return AWS.util.crypto.sha256(string, 'hex');
    },
  
    hexEncodedBodyHash: function hexEncodedBodyHash() {
      if (this.request.headers['X-Amz-Content-Sha256']) {
        return this.request.headers['X-Amz-Content-Sha256'];
      } else {
        return this.hexEncodedHash(this.request.body || '');
      }
    },
  
    unsignableHeaders: ['authorization', 'content-type', 'user-agent',
                        'x-amz-user-agent', 'x-amz-content-sha256'],
  
    isSignableHeader: function isSignableHeader(key) {
      return this.unsignableHeaders.indexOf(key) < 0;
    }
  
  });
  
  module.exports = AWS.Signers.V4;
  