Source: security/v2/validation-policy-command-interest.js

/**
 * Copyright (C) 2018 Regents of the University of California.
 * @author: Jeff Thompson <[email protected]>
 * @author: From ndn-cxx security https://github.com/named-data/ndn-cxx/blob/master/src/security/v2/validation-policy-command-interest.cpp
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * A copy of the GNU Lesser General Public License is in the file COPYING.
 */

/** @ignore */
var Name = require('../../name.js').Name; /** @ignore */
var Data = require('../../data.js').Data; /** @ignore */
var CommandInterestSigner = require('../command-interest-signer.js').CommandInterestSigner; /** @ignore */
var ValidationError = require('./validation-error.js').ValidationError; /** @ignore */
var ValidationPolicy = require('./validation-policy.js').ValidationPolicy;

/**
 * ValidationPolicyCommandInterest extends ValidationPolicy as a policy for
 * stop-and-wait command Interests. See:
 * https://redmine.named-data.net/projects/ndn-cxx/wiki/CommandInterest
 *
 * This policy checks the timestamp field of a stop-and-wait command Interest.
 * Signed Interest validation and Data validation requests are delegated to an
 * inner policy.
 *
 * Create a ValidationPolicyCommandInterest.
 * @param {ValidationPolicy} innerPolicy a ValidationPolicy for signed Interest
 * signature validation and Data validation. This must not be null.
 * @param {ValidationPolicyCommandInterest.Options} options (optional) The
 * stop-and-wait command Interest validation options. If omitted, use a default
 * Options().
 * @throws Error if innerPolicy is null.
 * @constructor
 */
var ValidationPolicyCommandInterest = function ValidationPolicyCommandInterest
  (innerPolicy, options)
{
  // Call the base constructor.
  ValidationPolicy.call(this);

  if (options == undefined)
    this.options_ = new ValidationPolicyCommandInterest.Options();
  else
    // Copy the Options.
    this.options_ = new ValidationPolicyCommandInterest.Options(options);

  this.container_ = []; // of ValidationPolicyCommandInterest.LastTimestampRecord
  this.nowOffsetMilliseconds_ = 0;

  if (innerPolicy == null)
    throw new Error("inner policy is missing");

  this.setInnerPolicy(innerPolicy);

  if (this.options_.gracePeriod_ < 0.0)
    this.options_.gracePeriod_ = 0.0;
};

ValidationPolicyCommandInterest.prototype = new ValidationPolicy();
ValidationPolicyCommandInterest.prototype.name = "ValidationPolicyCommandInterest";

exports.ValidationPolicyCommandInterest = ValidationPolicyCommandInterest;

/**
 * Create a ValidationPolicyCommandInterest.Options with the values.
 * @param {number|ValidationPolicyCommandInterest.Options} gracePeriodOrOptions
 * (optional) The tolerance of the initial timestamp in milliseconds. (However,
 * if this is another ValidationPolicyCommandInterest.Options, then copy values
 * from it.) If omitted, use a grace period of 2 minutes. A stop-and-wait
 * command Interest is considered "initial" if the validator has not recorded
 * the last timestamp from the same public key, or when such knowledge has been
 * erased. For an initial command Interest, its timestamp is compared to the
 * current system clock, and the command Interest is rejected if the absolute
 * difference is greater than the grace interval. The grace period should be
 * positive. Setting this option to 0 or negative causes the validator to
 * require exactly the same timestamp as the system clock, which most likely
 * rejects all command Interests.
 * @param {number} maxRecords (optional) The maximum number of distinct public
 * keys of which to record the last timestamp. If omitted, use 1000. The
 * validator records the last timestamps for every public key. For a subsequent
 * command Interest using the same public key, its timestamp is compared to the
 * last timestamp from that public key, and the command Interest is rejected if
 * its timestamp is less than or equal to the recorded timestamp.
 * This option limits the number of distinct public keys being tracked. If the
 * limit is exceeded, then the oldest record is deleted.
 * Setting max records to -1 allows tracking unlimited public keys. Setting max
 * records to 0 disables using last timestamp records and causes every command
 * Interest to be processed as initial.
 * @param {number} recordLifetime (optional) The maximum lifetime of a last
 * timestamp record in milliseconds. If omitted, use 1 hour. A last timestamp
 * record expires and can be deleted if it has not been refreshed within the
 * record lifetime. Setting the record lifetime to 0 or negative makes last
 * timestamp records expire immediately and causes every command Interest to be
 * processed as initial.
 * @constructor
 */
ValidationPolicyCommandInterest.Options = function ValidationPolicyCommandInterestOptions
  (gracePeriodOrOptions, maxRecords, recordLifetime)
{
  if (gracePeriodOrOptions instanceof ValidationPolicyCommandInterest.Options) {
    // The copy constructor.
    var options = gracePeriodOrOptions;

    this.gracePeriod_ = options.gracePeriod_;
    this.maxRecords_ = options.maxRecords_;
    this.recordLifetime_ = options.recordLifetime_;
  }
  else {
    var gracePeriod = gracePeriodOrOptions;

    if (gracePeriod == undefined)
      gracePeriod = 2 * 60 * 1000.0;
    if (maxRecords == undefined)
      maxRecords = 1000;
    if (recordLifetime == undefined)
      recordLifetime = 3600 * 1000.0;

    this.gracePeriod_ = gracePeriod;
    this.maxRecords_ = maxRecords;
    this.recordLifetime_ = recordLifetime;
  }
};

/**
 * @param {Data|Interest} dataOrInterest
 * @param {ValidationState} state
 * @param {function} continueValidation
 */
ValidationPolicyCommandInterest.prototype.checkPolicy = function
  (dataOrInterest, state, continueValidation)
{
  if (dataOrInterest instanceof Data) {
    var data = dataOrInterest;
    this.getInnerPolicy().checkPolicy(data, state, continueValidation);
  }
  else {
    var interest = dataOrInterest;

    var keyName = [null];
    var timestamp = [0];
    if (!ValidationPolicyCommandInterest.parseCommandInterest_
        (interest, state, keyName, timestamp))
      return;

    if (!this.checkTimestamp_(state, keyName[0], timestamp[0]))
      return;

    this.getInnerPolicy().checkPolicy(interest, state, continueValidation);
  }
};

/**
 * Set the offset when insertNewRecord_() and cleanUp_() get the current time,
 * which should only be used for testing.
 * @param {number} nowOffsetMilliseconds The offset in milliseconds.
 */
ValidationPolicyCommandInterest.prototype.setNowOffsetMilliseconds_ = function
  (nowOffsetMilliseconds)
{
  this.nowOffsetMilliseconds_ = nowOffsetMilliseconds;
};

/**
 * @param {Name} keyName
 * @param {number} timestamp
 * @param {number} lastRefreshed
 * @constructor
 */
ValidationPolicyCommandInterest.LastTimestampRecord =
  function ValidationPolicyCommandInterestLastTimestampRecord
  (keyName, timestamp, lastRefreshed)
{
  // Copy the Name.
  this.keyName_ = new Name(keyName);
  this.timestamp_ = timestamp;
  this.lastRefreshed_ = lastRefreshed;
};

ValidationPolicyCommandInterest.prototype.cleanUp_ = function()
{
  // nowOffsetMilliseconds_ is only used for testing.
  var now = new Date().getTime() + this.nowOffsetMilliseconds_;
  var expiring = now - this.options_.recordLifetime_;

  while ((this.container_.length > 0 &&
           this.container_[0].lastRefreshed_ <= expiring) ||
         (this.options_.maxRecords_ >= 0 &&
           this.container_.length > this.options_.maxRecords_))
    this.container_.shift();
};

/**
 * Get the keyLocatorName and timestamp from the command interest.
 * @param {Interest} interest The Interest to parse.
 * @param {ValidationState} state On error, this calls state.fail and returns
 * false.
 * @param {Array<Name>} keyLocatorName Set keyLocatorName[0] to the KeyLocator
 * name.
 * @param {Array<number>} timestamp Set timestamp[0] to the timestamp as
 * milliseconds since Jan 1, 1970 UTC.
 * @return {boolean} On success, return true. On error, call state.fail and
 * return false.
 */
ValidationPolicyCommandInterest.parseCommandInterest_ = function
  (interest, state, keyLocatorName, timestamp)
{
  keyLocatorName[0] = new Name();
  timestamp[0] = 0;

  var name = interest.getName();
  if (name.size() < CommandInterestSigner.MINIMUM_SIZE) {
    state.fail(new ValidationError(ValidationError.POLICY_ERROR,
      "Command interest name `" + interest.getName().toUri() + "` is too short"));
    return false;
  }

  timestamp[0] = name.get(CommandInterestSigner.POS_TIMESTAMP).toNumber();

  keyLocatorName[0] = ValidationPolicy.getKeyLocatorName(interest, state);
  if (state.isOutcomeFailed())
    // Already failed.
    return false;

  return true;
};

/**
 * @param {ValidationState} state On error, this calls state.fail and returns
 * false.
 * @param {Name} keyName The key name.
 * @param {number} timestamp The timestamp as milliseconds since Jan 1, 1970 UTC.
 * @return {boolean} On success, return true. On error, call state.fail and
 * return false.
 */
ValidationPolicyCommandInterest.prototype.checkTimestamp_ = function
  (state, keyName, timestamp)
{
  this.cleanUp_();

  // nowOffsetMilliseconds_ is only used for testing.
  var now = new Date().getTime() + this.nowOffsetMilliseconds_;
  if (timestamp < now - this.options_.gracePeriod_ ||
      timestamp > now + this.options_.gracePeriod_) {
    state.fail(new ValidationError(ValidationError.POLICY_ERROR,
      "Timestamp is outside the grace period for key " + keyName.toUri()));
    return false;
  }

  var index = this.findByKeyName_(keyName);
  if (index >= 0) {
    if (timestamp <= this.container_[index].timestamp_) {
      state.fail(new ValidationError(ValidationError.POLICY_ERROR,
        "Timestamp is reordered for key " + keyName.toUri()));
      return false;
    }
  }

  var thisPolicy = this;
  state.addSuccessCallback
    (function(interest) {
      thisPolicy.insertNewRecord_(interest, keyName, timestamp);
    });

  return true;
};

/**
 * @param {Interest} interest
 * @param {Name} keyName
 * @param {number} timestamp
 */
ValidationPolicyCommandInterest.prototype.insertNewRecord_ = function
  (interest, keyName, timestamp)
{
  // nowOffsetMilliseconds_ is only used for testing.
  var now = new Date().getTime() + this.nowOffsetMilliseconds_;
  var newRecord = new ValidationPolicyCommandInterest.LastTimestampRecord
    (keyName, timestamp, now);

  var index = this.findByKeyName_(keyName);
  if (index >= 0)
    // Remove the existing record so we can move it to the end.
    this.container_.splice(index, 1);

  this.container_.push(newRecord);
};

/**
 * Find the record in container_ which has the keyName.
 * @param {Name} keyName The key name to search for.
 * @return {number} The index in container_ of the record, or -1 if not found.
 */
ValidationPolicyCommandInterest.prototype.findByKeyName_ = function(keyName)
{
  for (var i = 0; i < this.container_.length; ++i) {
    if (this.container_[i].keyName_.equals(keyName))
      return i;
  }

  return -1;
};