<commit_before>/* Copyright 2019 Open Ag Data Alliance
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

const Promise = require('bluebird');
const request = Promise.promisifyAll(require('superagent'));
const jwku = Promise.promisifyAll(require('jwks-utils'));
const jws = require('jws');
const jwt = require('jsonwebtoken');
const jwk2pem = require('pem-jwk').jwk2pem;
const warn  = require('debug')('oada-trusted-jws:warn');
const info  = require('debug')('oada-trusted-jws:info');
const trace = require('debug')('oada-trusted-jws:trace');

var TRUSTED_LIST_URI = 'https://oada.github.io/oada-trusted-lists/client-registration-v2.json';

// callback <= (err, trusted, payload)
// options: {
//   timeout: 1000, // ms
//   trustedListCacheTime: 3600, // seconds
//   additionalTrustedListURIs: [ 'https://somewhere.com/client-registration.json' ],
// }

const trustedListCache = {};
const clearCache = function() {
  trustedListCache = {}; // Clear our cache of trusted lists
  jwku.clearJWKsCache(); // and clear the jwku library's cache of jwks sets
}                        // mainly useful for testing...

module.exports = function(sig, options, callback) {
  return Promise.try(() => {
    if (typeof options === 'function') {
      callback = options;
      options = undefined;
    }
    options = options || {};
    // Since 0 is a valid timeout, have to check whether it's actually a number instead of just truthy
    options.timeout = typeof options.timeout === 'number' ? options.timeout : 1000;
    // Default trusted list cached for one hour, in seconds
    options.trustedListCacheTime = typeof options.trustedListCacheTime === 'number' ? options.trustedListCacheTime : 3600; // seconds
    options.additionalTrustedListURIs = options.additionalTrustedListURIs || [];
    options.disableDefaultTrustedListURI = options.disableDefaultTrustedListURI || false;

    // Build the list of all the trusted lists we're going to check
    let trustedListURIs = options.disableDefaultTrustedListURI ? [] : [ TRUSTED_LIST_URI ];
    trace('additionalTrustedListURIs = ', options.additionalTrustedListURIs);
    trustedListURIs = trustedListURIs.concat(options.additionalTrustedListURIs);
    trace('Using trustedListURIs = ', trustedListURIs);

    //---------------------------------------------------------------------------
    // Loop over all the trusted list URI's, checking if we already have in cache
    // If in cache, also check that they are not stale and need to be replaced
    trace('Starting trusted lists cache check, trustedListCache = ', trustedListCache);
    const now = Date.now() / 1000; // convert ms to sec
    return Promise.map(trustedListURIs, listURI => {
      if (  !trustedListCache[listURI]
          || trustedListCache[listURI].timeLastFetched < (now - options.trustedListCacheTime)) { // either not cached, or cache is old
        trace('listURI ',listURI,' is not in cache or is stale, fetching...');
        return request.get(listURI)
         .timeout(options.timeout)
         .then(result => {
           const newCacheObj = {
             timeLastFetched: now,
             body: result.body,
             listURI,
           };
           trustedListCache[listURI] = newCacheObj;
           trace('Fetched list from URI ',listURI, ', putting this into the cache: ', newCacheObj);
           return newCacheObj;
         }).catch(err => {
           warn('WARNING: unable to fetch trusted list at URI.',listURI);
           return false;
         });
      }
      // else, we have it in the cache, so return the cached body directly
      trace('listURI ', listURI, ' is in cache, returning cached value: ', trustedListCache[listURI]);
      return Promise.resolve(trustedListCache[listURI]);
    });

  //-----------------------------------------------------------------------------
  // Now, look through all the lists to see if the jku on the signature is in any of the trusted lists
  }).then(lists => {
    trace('List caching section finished, lists = ', lists);
    // jws.decode throws if the signature is invalid
    try {
      var decoded = jws.decode(sig);
    } catch(err) {
      decoded = false;
    }
    if (!decoded || !decoded.header) {
      trace('decoded signature is null or has no header.');
      return { decoded: false, trusted: false, jwk: false };
    }
    trace('Tried decoding the signature, resulting in decoded = ', decoded);

    // Now look in the list for a jku or jwk that matches the one on this signature:
    const foundList = lists.reduce((alreadyFound,l) => {
      if (alreadyFound) return alreadyFound;
      if (!l || !l.body) return false;
      if (l.body.version === "2") {
        // v2 trusted list: an object with a list of jku's and/or jwk's
        if (!l.body.jkus && !l.body.jwks) return false; // have neither jkus in trusted list nor jwks
        // Check jku list to see if we have a match in this header:
        let foundJKU = false;
        if (typeof l.body.jkus.find === 'function') {
          // If jkus is a list of strings of trusted URL's, see if it matches jku in header:
          foundJKU = l.body.jkus.find(i => (typeof i === 'string' && i.length > 0 && i === decoded.header.jku));
        }
        // Check jwks key set in trusted list if there is one
        let foundJWKInJWKS = false;
        if (jwku.isJWKset(l.body.jwks)) {
          // Search through the trusted JWKS set
          if(jwku.findJWK(decoded.header.jwk && decoded.header.jwk.kid, l.body.jwks)) {
            foundJWKInJWKS = l.body.jwks; // keep the JWKS to use later in checking signature
          }
        }
        trace('Searched list '+l.listURI+' for jwk or jku from header, foundJKU = ', foundJKU, ', foundJWKInJWKS = ', foundJWKInJWKS);
        // returns either a JKU string, or a JWKS object
        return foundJKU || foundJWKInJWKS;
      }
      // v1 trusted list: an array of strings that are all jku's (no jwk's supported in trusted list)
      if (typeof l.body.find !== 'function') return false; // not an array
      return l.body.find(i => (i === decoded.header.jku)); // returns jku string
    // initial value of alreadyFound in reducer is false:
    }, false);
    if (!foundList) {
      info('header of decoded signature does not have a jku or jwk key that '
          +'exists in any of the trusted lists. decoded.header = ', decoded.header);
    }
    // foundList is now either a string (jku) or object (trusted jwks)
    trace('Result of search for jku or jwk that matches a trusted list entry = ', foundList);

    // If we found the jku from
    // the header in a trusted list, then the call below will tell jwkForSignatureAsync to
    // use that jku, go there and get the list of keys, then use the kid to lookup the jwk.
    // If it was not found in a trusted list, then jwkForSignatureAsync will just return either
    // the jwk from the header directly or the corresponding jwk from a jku lookup
    return Promise.props({
      decoded,
      // IMPORTANT: !!foundList at this point does not know if the signature actually is valid and trusted,
      // it only knows that the signature pointed at something in a trusted list.  We don't really know if
      // it is trusted until we check both that the signature pointed at something in a trusted list, AND
      // the signature was signed with the private key of the trusted thing it pointed at.
      // Therefore, in the next .then() block when we call verify with the jwk from here, if it throws
      // then we know the signature couldn't be verified and will therefore be considered untrusted
      trusted: !!foundList,  // true/false whether trusted/not_trusted
      jwk: jwku.jwkForSignatureAsync(sig, !!foundList ? foundList : false, { timeout: options.timeout }),
    });

  // Now we can go ahead and verify the signature with the jwk:
  }).then(({ decoded, trusted, jwk }) => {
    if (!decoded) return [ false, false ];
    return [ trusted, jwt.verify(sig, jwk2pem(jwk)) ];

  // And signal the callback with any thrown errors, or (trusted, clientRegistration) if no errors
  }).nodeify(callback, { spread: true });

};

module.exports.TRUSTED_LIST_URI = TRUSTED_LIST_URI;
module.exports.clearCache = clearCache;
<commit_msg>Fixed bug in clearCache<commit_after>/* Copyright 2019 Open Ag Data Alliance
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

const Promise = require('bluebird');
const request = Promise.promisifyAll(require('superagent'));
const jwku = Promise.promisifyAll(require('jwks-utils'));
const jws = require('jws');
const jwt = require('jsonwebtoken');
const jwk2pem = require('pem-jwk').jwk2pem;
const warn  = require('debug')('oada-trusted-jws:warn');
const info  = require('debug')('oada-trusted-jws:info');
const trace = require('debug')('oada-trusted-jws:trace');

var TRUSTED_LIST_URI = 'https://oada.github.io/oada-trusted-lists/client-registration-v2.json';

// callback <= (err, trusted, payload)
// options: {
//   timeout: 1000, // ms
//   trustedListCacheTime: 3600, // seconds
//   additionalTrustedListURIs: [ 'https://somewhere.com/client-registration.json' ],
// }

let trustedListCache = {};
const clearCache = function() {
  trustedListCache = {}; // Clear our cache of trusted lists
  jwku.clearJWKsCache(); // and clear the jwku library's cache of jwks sets
}                        // mainly useful for testing...

module.exports = function(sig, options, callback) {
  return Promise.try(() => {
    if (typeof options === 'function') {
      callback = options;
      options = undefined;
    }
    options = options || {};
    // Since 0 is a valid timeout, have to check whether it's actually a number instead of just truthy
    options.timeout = typeof options.timeout === 'number' ? options.timeout : 1000;
    // Default trusted list cached for one hour, in seconds
    options.trustedListCacheTime = typeof options.trustedListCacheTime === 'number' ? options.trustedListCacheTime : 3600; // seconds
    options.additionalTrustedListURIs = options.additionalTrustedListURIs || [];
    options.disableDefaultTrustedListURI = options.disableDefaultTrustedListURI || false;

    // Build the list of all the trusted lists we're going to check
    let trustedListURIs = options.disableDefaultTrustedListURI ? [] : [ TRUSTED_LIST_URI ];
    trace('additionalTrustedListURIs = ', options.additionalTrustedListURIs);
    trustedListURIs = trustedListURIs.concat(options.additionalTrustedListURIs);
    trace('Using trustedListURIs = ', trustedListURIs);

    //---------------------------------------------------------------------------
    // Loop over all the trusted list URI's, checking if we already have in cache
    // If in cache, also check that they are not stale and need to be replaced
    trace('Starting trusted lists cache check, trustedListCache = ', trustedListCache);
    const now = Date.now() / 1000; // convert ms to sec
    return Promise.map(trustedListURIs, listURI => {
      if (  !trustedListCache[listURI]
          || trustedListCache[listURI].timeLastFetched < (now - options.trustedListCacheTime)) { // either not cached, or cache is old
        trace('listURI ',listURI,' is not in cache or is stale, fetching...');
        return request.get(listURI)
         .timeout(options.timeout)
         .then(result => {
           const newCacheObj = {
             timeLastFetched: now,
             body: result.body,
             listURI,
           };
           trustedListCache[listURI] = newCacheObj;
           trace('Fetched list from URI ',listURI, ', putting this into the cache: ', newCacheObj);
           return newCacheObj;
         }).catch(err => {
           warn('WARNING: unable to fetch trusted list at URI.',listURI);
           return false;
         });
      }
      // else, we have it in the cache, so return the cached body directly
      trace('listURI ', listURI, ' is in cache, returning cached value: ', trustedListCache[listURI]);
      return Promise.resolve(trustedListCache[listURI]);
    });

  //-----------------------------------------------------------------------------
  // Now, look through all the lists to see if the jku on the signature is in any of the trusted lists
  }).then(lists => {
    trace('List caching section finished, lists = ', lists);
    // jws.decode throws if the signature is invalid
    try {
      var decoded = jws.decode(sig);
    } catch(err) {
      decoded = false;
    }
    if (!decoded || !decoded.header) {
      trace('decoded signature is null or has no header.');
      return { decoded: false, trusted: false, jwk: false };
    }
    trace('Tried decoding the signature, resulting in decoded = ', decoded);

    // Now look in the list for a jku or jwk that matches the one on this signature:
    const foundList = lists.reduce((alreadyFound,l) => {
      if (alreadyFound) return alreadyFound;
      if (!l || !l.body) return false;
      if (l.body.version === "2") {
        // v2 trusted list: an object with a list of jku's and/or jwk's
        if (!l.body.jkus && !l.body.jwks) return false; // have neither jkus in trusted list nor jwks
        // Check jku list to see if we have a match in this header:
        let foundJKU = false;
        if (typeof l.body.jkus.find === 'function') {
          // If jkus is a list of strings of trusted URL's, see if it matches jku in header:
          foundJKU = l.body.jkus.find(i => (typeof i === 'string' && i.length > 0 && i === decoded.header.jku));
        }
        // Check jwks key set in trusted list if there is one
        let foundJWKInJWKS = false;
        if (jwku.isJWKset(l.body.jwks)) {
          // Search through the trusted JWKS set
          if(jwku.findJWK(decoded.header.jwk && decoded.header.jwk.kid, l.body.jwks)) {
            foundJWKInJWKS = l.body.jwks; // keep the JWKS to use later in checking signature
          }
        }
        trace('Searched list '+l.listURI+' for jwk or jku from header, foundJKU = ', foundJKU, ', foundJWKInJWKS = ', foundJWKInJWKS);
        // returns either a JKU string, or a JWKS object
        return foundJKU || foundJWKInJWKS;
      }
      // v1 trusted list: an array of strings that are all jku's (no jwk's supported in trusted list)
      if (typeof l.body.find !== 'function') return false; // not an array
      return l.body.find(i => (i === decoded.header.jku)); // returns jku string
    // initial value of alreadyFound in reducer is false:
    }, false);
    if (!foundList) {
      info('header of decoded signature does not have a jku or jwk key that '
          +'exists in any of the trusted lists. decoded.header = ', decoded.header);
    }
    // foundList is now either a string (jku) or object (trusted jwks)
    trace('Result of search for jku or jwk that matches a trusted list entry = ', foundList);

    // If we found the jku from
    // the header in a trusted list, then the call below will tell jwkForSignatureAsync to
    // use that jku, go there and get the list of keys, then use the kid to lookup the jwk.
    // If it was not found in a trusted list, then jwkForSignatureAsync will just return either
    // the jwk from the header directly or the corresponding jwk from a jku lookup
    return Promise.props({
      decoded,
      // IMPORTANT: !!foundList at this point does not know if the signature actually is valid and trusted,
      // it only knows that the signature pointed at something in a trusted list.  We don't really know if
      // it is trusted until we check both that the signature pointed at something in a trusted list, AND
      // the signature was signed with the private key of the trusted thing it pointed at.
      // Therefore, in the next .then() block when we call verify with the jwk from here, if it throws
      // then we know the signature couldn't be verified and will therefore be considered untrusted
      trusted: !!foundList,  // true/false whether trusted/not_trusted
      jwk: jwku.jwkForSignatureAsync(sig, !!foundList ? foundList : false, { timeout: options.timeout }),
    });

  // Now we can go ahead and verify the signature with the jwk:
  }).then(({ decoded, trusted, jwk }) => {
    if (!decoded) return [ false, false ];
    return [ trusted, jwt.verify(sig, jwk2pem(jwk)) ];

  // And signal the callback with any thrown errors, or (trusted, clientRegistration) if no errors
  }).nodeify(callback, { spread: true });

};

module.exports.TRUSTED_LIST_URI = TRUSTED_LIST_URI;
module.exports.clearCache = clearCache;
<|endoftext|>


<commit_before>var fs = require('fs');
var path = require('path');

var CleanCSS = require('clean-css');
var commands = require('commander');

function cli(process) {
  var packageConfig = fs.readFileSync(path.join(path.dirname(fs.realpathSync(process.argv[1])), '../package.json'));
  var buildVersion = JSON.parse(packageConfig).version;
  var fromStdin;
  var debugMode;
  var options;
  var stdin;
  var data;

  // Specify commander options to parse command line params correctly
  commands
    .version(buildVersion, '-v, --version')
    .usage('[options] <source-file ...>')
    .option('-c, --compatibility [ie7|ie8]', 'Force compatibility mode (see Readme for advanced examples)')
    .option('-d, --debug', 'Shows debug information (minification time & compression efficiency)')
    .option('-f, --format <options>', 'Controls output formatting, see examples below')
    .option('-o, --output [output-file]', 'Use [output-file] as output instead of STDOUT')
    .option('-O <n> [optimizations]', 'Turn on level <n> optimizations; optionally accepts a list of fine-grained options, defaults to `1`, see examples below', function (val) { return Math.abs(parseInt(val)); })
    .option('--inline [rules]', 'Enables inlining for listed sources (defaults to `local`)')
    .option('--inline-timeout [seconds]', 'Per connection timeout when fetching remote stylesheets (defaults to 5 seconds)', parseFloat)
    .option('--skip-rebase', 'Disable URLs rebasing')
    .option('--source-map', 'Enables building input\'s source map')
    .option('--source-map-inline-sources', 'Enables inlining sources inside source maps');

  commands.on('--help', function () {
    console.log('  Examples:\n');
    console.log('    %> cleancss one.css');
    console.log('    %> cleancss -o one-min.css one.css');
    console.log('    %> cleancss -o merged-and-minified.css one.css two.css three.css');
    console.log('    %> cleancss one.css two.css three.css | gzip -9 -c > merged-minified-and-gzipped.css.gz');
    console.log('');
    console.log('  Formatting options:');
    console.log('    %> cleancss --format beautify one.css');
    console.log('    %> cleancss --format keep-breaks one.css');
    console.log('    %> cleancss --format \'indentBy:1;indentWith:tab\' one.css');
    console.log('    %> cleancss --format \'breaks:afterBlockBegins=on;spaces:aroundSelectorRelation=on\' one.css');
    console.log('    %> # `breaks` controls where to insert breaks');
    console.log('    %> #   `afterAtRule` controls if a line break comes after an at-rule; e.g. `@charset`; defaults to `off` (alias to `false`)');
    console.log('    %> #   `afterBlockBegins` controls if a line break comes after a block begins; e.g. `@media`; defaults to `off`');
    console.log('    %> #   `afterBlockEnds` controls if a line break comes after a block ends, defaults to `off`');
    console.log('    %> #   `afterComment` controls if a line break comes after a comment; defaults to `off`');
    console.log('    %> #   `afterProperty` controls if a line break comes after a property; defaults to `off`');
    console.log('    %> #   `afterRuleBegins` controls if a line break comes after a rule begins; defaults to `off`');
    console.log('    %> #   `afterRuleEnds` controls if a line break comes after a rule ends; defaults to `off`');
    console.log('    %> #   `beforeBlockEnds` controls if a line break comes before a block ends; defaults to `off`');
    console.log('    %> #   `betweenSelectors` controls if a line break comes between selectors; defaults to `off`');
    console.log('    %> # `indentBy` controls number of characters to indent with; defaults to `0`');
    console.log('    %> # `indentWith` controls a character to indent with, can be `space` or `tab`; defaults to `space`');
    console.log('    %> # `spaces` controls where to insert spaces');
    console.log('    %> #   `aroundSelectorRelation` controls if spaces come around selector relations; e.g. `div > a`; defaults to `off`');
    console.log('    %> #   `beforeBlockBegins` controls if a space comes before a block begins; e.g. `.block {`; defaults to `off`');
    console.log('    %> #   `beforeValue` controls if a space comes before a value; e.g. `width: 1rem`; defaults to `off`');
    console.log('    %> # `wrapAt` controls maximum line length; defaults to `off`');
    console.log('');
    console.log('  Level 0 optimizations:');
    console.log('    %> cleancss -O0 one.css');
    console.log('');
    console.log('  Level 1 optimizations:');
    console.log('    %> cleancss -O1 one.css');
    console.log('    %> cleancss -O1 removeQuotes:off;roundingPrecision:4;specialComments:1 one.css');
    console.log('    %> cleancss -O1 all:off;specialComments:1 one.css');
    console.log('    %> # `cleanupCharsets` controls `@charset` moving to the front of a stylesheet; defaults to `on`');
    console.log('    %> # `normalizeUrls` controls URL normalzation; default to `on`');
    console.log('    %> # `optimizeBackground` controls `background` property optimizatons; defaults to `on`');
    console.log('    %> # `optimizeBorderRadius` controls `border-radius` property optimizatons; defaults to `on`');
    console.log('    %> # `optimizeFilter` controls `filter` property optimizatons; defaults to `on`');
    console.log('    %> # `optimizeFontWeight` controls `font-weight` property optimizatons; defaults to `on`');
    console.log('    %> # `optimizeOutline` controls `outline` property optimizatons; defaults to `on`');
    console.log('    %> # `removeEmpty` controls removing empty rules and nested blocks; defaults to `on` (since 4.1.0)');
    console.log('    %> # `removeNegativePaddings` controls removing negative paddings; defaults to `on`');
    console.log('    %> # `removeQuotes` controls removing quotes when unnecessary; defaults to `on`');
    console.log('    %> # `removeWhitespace` controls removing unused whitespace; defaults to `on`');
    console.log('    %> # `replaceMultipleZeros` contols removing redundant zeros; defaults to `on`');
    console.log('    %> # `replaceTimeUnits` controls replacing time units with shorter values; defaults to `on');
    console.log('    %> # `replaceZeroUnits` controls replacing zero values with units; defaults to `on`');
    console.log('    %> # `roundingPrecision` rounds pixel values to `N` decimal places; `off` disables rounding; defaults to `off`');
    console.log('    %> # `selectorsSortingMethod` denotes selector sorting method; can be `natural` or `standard`; defaults to `standard`');
    console.log('    %> # `specialComments` denotes a number of /*! ... */ comments preserved; defaults to `all`');
    console.log('    %> # `tidyAtRules` controls at-rules (e.g. `@charset`, `@import`) optimizing; defaults to `on`');
    console.log('    %> # `tidyBlockScopes` controls block scopes (e.g. `@media`) optimizing; defaults to `on`');
    console.log('    %> # `tidySelectors` controls selectors optimizing; defaults to `on`');
    console.log('');
    console.log('  Level 2 optimizations:');
    console.log('    %> cleancss -O2 one.css');
    console.log('    %> cleancss -O2 mergeMedia:off;restructureRules:off;mergeSemantically:on;mergeIntoShorthands:off one.css');
    console.log('    %> cleancss -O2 all:off;removeDuplicateRules:on one.css');
    console.log('    %> # `mergeAdjacentRules` controls adjacent rules merging; defaults to `on`');
    console.log('    %> # `mergeIntoShorthands` controls merging properties into shorthands; defaults to `on`');
    console.log('    %> # `mergeMedia` controls `@media` merging; defaults to `on`');
    console.log('    %> # `mergeNonAdjacentRules` controls non-adjacent rule merging; defaults to `on`');
    console.log('    %> # `mergeSemantically` controls semantic merging; defaults to `off`');
    console.log('    %> # `overrideProperties` controls property overriding based on understandability; defaults to `on`');
    console.log('    %> # `reduceNonAdjacentRules` controls non-adjacent rule reducing; defaults to `on`');
    console.log('    %> # `removeDuplicateFontRules` controls duplicate `@font-face` removing; defaults to `on`');
    console.log('    %> # `removeDuplicateMediaBlocks` controls duplicate `@media` removing; defaults to `on`');
    console.log('    %> # `removeDuplicateRules` controls duplicate rules removing; defaults to `on`');
    console.log('    %> # `removeEmpty` controls removing empty rules and nested blocks; defaults to `on` (since 4.1.0)');
    console.log('    %> # `removeUnusedAtRules` controls unused at rule removing; defaults to `off` (since 4.1.0)');
    console.log('    %> # `restructureRules` controls rule restructuring; defaults to `off`');
    console.log('    %> # `skipProperties` controls which properties won\'t be optimized, defaults to empty list which means all will be optimized (since 4.1.0)');

    process.exit();
  });

  commands.parse(process.argv);

  if (commands.rawArgs.indexOf('-O0') > -1) {
    commands.O0 = true;
  }

  if (commands.rawArgs.indexOf('-O1') > -1) {
    commands.O1 = findArgumentTo('-O1', commands.rawArgs, commands.args);
  }

  if (commands.rawArgs.indexOf('-O2') > -1) {
    commands.O2 = findArgumentTo('-O2', commands.rawArgs, commands.args);
  }

  // If no sensible data passed in just print help and exit
  fromStdin = !process.env.__DIRECT__ && !process.stdin.isTTY;
  if (!fromStdin && commands.args.length === 0) {
    commands.outputHelp();
    return 0;
  }

  // Now coerce commands into CleanCSS configuration...
  debugMode = commands.debug;
  options = {
    compatibility: commands.compatibility,
    format: commands.format,
    inline: commands.inline || 'local',
    inlineTimeout: commands.inlineTimeout * 1000,
    level: commands.O0 || commands.O1 || commands.O2 ?
      { '0': commands.O0, '1': commands.O1, '2': commands.O2 } :
      undefined,
    output: commands.output,
    rebase: commands.skipRebase ? false : true,
    rebaseTo: ('output' in commands) && commands.output.length > 0 ? path.dirname(path.resolve(commands.output)) : process.cwd(),
    sourceMap: commands.sourceMap,
    sourceMapInlineSources: commands.sourceMapInlineSources
  };

  if (options.sourceMap && !options.output) {
    outputFeedback(['Source maps will not be built because you have not specified an output file.'], true);
    options.sourceMap = false;
  }

  // ... and do the magic!
  if (commands.args.length > 0) {
    minify(commands.args);
  } else {
    stdin = process.openStdin();
    stdin.setEncoding('utf-8');
    data = '';
    stdin.on('data', function (chunk) {
      data += chunk;
    });
    stdin.on('end', function () {
      minify(data);
    });
  }

  function findArgumentTo(option, rawArgs, args) {
    var value = true;
    var optionAt = rawArgs.indexOf(option);
    var nextOption = rawArgs[optionAt + 1];
    var looksLikePath;
    var asArgumentAt;

    if (!nextOption) {
      return value;
    }

    looksLikePath = nextOption.indexOf('.css') > -1 ||
      /\//.test(nextOption) ||
      /\\[^\-]/.test(nextOption) ||
      /^https?:\/\//.test(nextOption);
    asArgumentAt = args.indexOf(nextOption);

    if (!looksLikePath) {
      value = nextOption;
    }

    if (!looksLikePath && asArgumentAt > -1) {
      args.splice(asArgumentAt, 1);
    }

    return value;
  }

  function minify(data) {
    new CleanCSS(options).minify(data, function (errors, minified) {
      var mapFilename;

      if (debugMode) {
        console.error('Original: %d bytes', minified.stats.originalSize);
        console.error('Minified: %d bytes', minified.stats.minifiedSize);
        console.error('Efficiency: %d%', ~~(minified.stats.efficiency * 10000) / 100.0);
        console.error('Time spent: %dms', minified.stats.timeSpent);

        if (minified.inlinedStylesheets.length > 0) {
          console.error('Inlined stylesheets:');
          minified.inlinedStylesheets.forEach(function (uri) {
            console.error('- %s', uri);
          });
        }
      }

      outputFeedback(minified.errors, true);
      outputFeedback(minified.warnings);

      if (minified.errors.length > 0) {
        process.exit(1);
      }

      if (minified.sourceMap) {
        mapFilename = path.basename(options.output) + '.map';
        output(minified.styles + '/*# sourceMappingURL=' + mapFilename + ' */');
        outputMap(minified.sourceMap, mapFilename);
      } else {
        output(minified.styles);
      }
    });
  }

  function output(minified) {
    if (options.output) {
      fs.writeFileSync(options.output, minified, 'utf8');
    } else {
      process.stdout.write(minified);
    }
  }

  function outputMap(sourceMap, mapFilename) {
    var mapPath = path.join(path.dirname(options.output), mapFilename);
    fs.writeFileSync(mapPath, sourceMap.toString(), 'utf-8');
  }

  function outputFeedback(messages, isError) {
    var prefix = isError ? '\x1B[31mERROR\x1B[39m:' : 'WARNING:';

    messages.forEach(function (message) {
      console.error('%s %s', prefix, message);
    });
  }
}

module.exports = cli;
<commit_msg>See #7 - refactors CLI code from one big function.<commit_after>var fs = require('fs');
var path = require('path');

var CleanCSS = require('clean-css');
var commands = require('commander');

function cli(process) {
  var packageConfig = fs.readFileSync(path.join(path.dirname(fs.realpathSync(process.argv[1])), '../package.json'));
  var buildVersion = JSON.parse(packageConfig).version;
  var fromStdin;
  var debugMode;
  var options;
  var stdin;
  var data;

  // Specify commander options to parse command line params correctly
  commands
    .version(buildVersion, '-v, --version')
    .usage('[options] <source-file ...>')
    .option('-c, --compatibility [ie7|ie8]', 'Force compatibility mode (see Readme for advanced examples)')
    .option('-d, --debug', 'Shows debug information (minification time & compression efficiency)')
    .option('-f, --format <options>', 'Controls output formatting, see examples below')
    .option('-o, --output [output-file]', 'Use [output-file] as output instead of STDOUT')
    .option('-O <n> [optimizations]', 'Turn on level <n> optimizations; optionally accepts a list of fine-grained options, defaults to `1`, see examples below', function (val) { return Math.abs(parseInt(val)); })
    .option('--inline [rules]', 'Enables inlining for listed sources (defaults to `local`)')
    .option('--inline-timeout [seconds]', 'Per connection timeout when fetching remote stylesheets (defaults to 5 seconds)', parseFloat)
    .option('--skip-rebase', 'Disable URLs rebasing')
    .option('--source-map', 'Enables building input\'s source map')
    .option('--source-map-inline-sources', 'Enables inlining sources inside source maps');

  commands.on('--help', function () {
    console.log('  Examples:\n');
    console.log('    %> cleancss one.css');
    console.log('    %> cleancss -o one-min.css one.css');
    console.log('    %> cleancss -o merged-and-minified.css one.css two.css three.css');
    console.log('    %> cleancss one.css two.css three.css | gzip -9 -c > merged-minified-and-gzipped.css.gz');
    console.log('');
    console.log('  Formatting options:');
    console.log('    %> cleancss --format beautify one.css');
    console.log('    %> cleancss --format keep-breaks one.css');
    console.log('    %> cleancss --format \'indentBy:1;indentWith:tab\' one.css');
    console.log('    %> cleancss --format \'breaks:afterBlockBegins=on;spaces:aroundSelectorRelation=on\' one.css');
    console.log('    %> # `breaks` controls where to insert breaks');
    console.log('    %> #   `afterAtRule` controls if a line break comes after an at-rule; e.g. `@charset`; defaults to `off` (alias to `false`)');
    console.log('    %> #   `afterBlockBegins` controls if a line break comes after a block begins; e.g. `@media`; defaults to `off`');
    console.log('    %> #   `afterBlockEnds` controls if a line break comes after a block ends, defaults to `off`');
    console.log('    %> #   `afterComment` controls if a line break comes after a comment; defaults to `off`');
    console.log('    %> #   `afterProperty` controls if a line break comes after a property; defaults to `off`');
    console.log('    %> #   `afterRuleBegins` controls if a line break comes after a rule begins; defaults to `off`');
    console.log('    %> #   `afterRuleEnds` controls if a line break comes after a rule ends; defaults to `off`');
    console.log('    %> #   `beforeBlockEnds` controls if a line break comes before a block ends; defaults to `off`');
    console.log('    %> #   `betweenSelectors` controls if a line break comes between selectors; defaults to `off`');
    console.log('    %> # `indentBy` controls number of characters to indent with; defaults to `0`');
    console.log('    %> # `indentWith` controls a character to indent with, can be `space` or `tab`; defaults to `space`');
    console.log('    %> # `spaces` controls where to insert spaces');
    console.log('    %> #   `aroundSelectorRelation` controls if spaces come around selector relations; e.g. `div > a`; defaults to `off`');
    console.log('    %> #   `beforeBlockBegins` controls if a space comes before a block begins; e.g. `.block {`; defaults to `off`');
    console.log('    %> #   `beforeValue` controls if a space comes before a value; e.g. `width: 1rem`; defaults to `off`');
    console.log('    %> # `wrapAt` controls maximum line length; defaults to `off`');
    console.log('');
    console.log('  Level 0 optimizations:');
    console.log('    %> cleancss -O0 one.css');
    console.log('');
    console.log('  Level 1 optimizations:');
    console.log('    %> cleancss -O1 one.css');
    console.log('    %> cleancss -O1 removeQuotes:off;roundingPrecision:4;specialComments:1 one.css');
    console.log('    %> cleancss -O1 all:off;specialComments:1 one.css');
    console.log('    %> # `cleanupCharsets` controls `@charset` moving to the front of a stylesheet; defaults to `on`');
    console.log('    %> # `normalizeUrls` controls URL normalzation; default to `on`');
    console.log('    %> # `optimizeBackground` controls `background` property optimizatons; defaults to `on`');
    console.log('    %> # `optimizeBorderRadius` controls `border-radius` property optimizatons; defaults to `on`');
    console.log('    %> # `optimizeFilter` controls `filter` property optimizatons; defaults to `on`');
    console.log('    %> # `optimizeFontWeight` controls `font-weight` property optimizatons; defaults to `on`');
    console.log('    %> # `optimizeOutline` controls `outline` property optimizatons; defaults to `on`');
    console.log('    %> # `removeEmpty` controls removing empty rules and nested blocks; defaults to `on` (since 4.1.0)');
    console.log('    %> # `removeNegativePaddings` controls removing negative paddings; defaults to `on`');
    console.log('    %> # `removeQuotes` controls removing quotes when unnecessary; defaults to `on`');
    console.log('    %> # `removeWhitespace` controls removing unused whitespace; defaults to `on`');
    console.log('    %> # `replaceMultipleZeros` contols removing redundant zeros; defaults to `on`');
    console.log('    %> # `replaceTimeUnits` controls replacing time units with shorter values; defaults to `on');
    console.log('    %> # `replaceZeroUnits` controls replacing zero values with units; defaults to `on`');
    console.log('    %> # `roundingPrecision` rounds pixel values to `N` decimal places; `off` disables rounding; defaults to `off`');
    console.log('    %> # `selectorsSortingMethod` denotes selector sorting method; can be `natural` or `standard`; defaults to `standard`');
    console.log('    %> # `specialComments` denotes a number of /*! ... */ comments preserved; defaults to `all`');
    console.log('    %> # `tidyAtRules` controls at-rules (e.g. `@charset`, `@import`) optimizing; defaults to `on`');
    console.log('    %> # `tidyBlockScopes` controls block scopes (e.g. `@media`) optimizing; defaults to `on`');
    console.log('    %> # `tidySelectors` controls selectors optimizing; defaults to `on`');
    console.log('');
    console.log('  Level 2 optimizations:');
    console.log('    %> cleancss -O2 one.css');
    console.log('    %> cleancss -O2 mergeMedia:off;restructureRules:off;mergeSemantically:on;mergeIntoShorthands:off one.css');
    console.log('    %> cleancss -O2 all:off;removeDuplicateRules:on one.css');
    console.log('    %> # `mergeAdjacentRules` controls adjacent rules merging; defaults to `on`');
    console.log('    %> # `mergeIntoShorthands` controls merging properties into shorthands; defaults to `on`');
    console.log('    %> # `mergeMedia` controls `@media` merging; defaults to `on`');
    console.log('    %> # `mergeNonAdjacentRules` controls non-adjacent rule merging; defaults to `on`');
    console.log('    %> # `mergeSemantically` controls semantic merging; defaults to `off`');
    console.log('    %> # `overrideProperties` controls property overriding based on understandability; defaults to `on`');
    console.log('    %> # `reduceNonAdjacentRules` controls non-adjacent rule reducing; defaults to `on`');
    console.log('    %> # `removeDuplicateFontRules` controls duplicate `@font-face` removing; defaults to `on`');
    console.log('    %> # `removeDuplicateMediaBlocks` controls duplicate `@media` removing; defaults to `on`');
    console.log('    %> # `removeDuplicateRules` controls duplicate rules removing; defaults to `on`');
    console.log('    %> # `removeEmpty` controls removing empty rules and nested blocks; defaults to `on` (since 4.1.0)');
    console.log('    %> # `removeUnusedAtRules` controls unused at rule removing; defaults to `off` (since 4.1.0)');
    console.log('    %> # `restructureRules` controls rule restructuring; defaults to `off`');
    console.log('    %> # `skipProperties` controls which properties won\'t be optimized, defaults to empty list which means all will be optimized (since 4.1.0)');

    process.exit();
  });

  commands.parse(process.argv);

  if (commands.rawArgs.indexOf('-O0') > -1) {
    commands.O0 = true;
  }

  if (commands.rawArgs.indexOf('-O1') > -1) {
    commands.O1 = findArgumentTo('-O1', commands.rawArgs, commands.args);
  }

  if (commands.rawArgs.indexOf('-O2') > -1) {
    commands.O2 = findArgumentTo('-O2', commands.rawArgs, commands.args);
  }

  // If no sensible data passed in just print help and exit
  fromStdin = !process.env.__DIRECT__ && !process.stdin.isTTY;
  if (!fromStdin && commands.args.length === 0) {
    commands.outputHelp();
    return 0;
  }

  // Now coerce commands into CleanCSS configuration...
  debugMode = commands.debug;
  options = {
    compatibility: commands.compatibility,
    format: commands.format,
    inline: commands.inline || 'local',
    inlineTimeout: commands.inlineTimeout * 1000,
    level: commands.O0 || commands.O1 || commands.O2 ?
      { '0': commands.O0, '1': commands.O1, '2': commands.O2 } :
      undefined,
    output: commands.output,
    rebase: commands.skipRebase ? false : true,
    rebaseTo: ('output' in commands) && commands.output.length > 0 ? path.dirname(path.resolve(commands.output)) : process.cwd(),
    sourceMap: commands.sourceMap,
    sourceMapInlineSources: commands.sourceMapInlineSources
  };

  if (options.sourceMap && !options.output) {
    outputFeedback(['Source maps will not be built because you have not specified an output file.'], true);
    options.sourceMap = false;
  }

  // ... and do the magic!
  if (commands.args.length > 0) {
    minify(process, options, debugMode, commands.args);
  } else {
    stdin = process.openStdin();
    stdin.setEncoding('utf-8');
    data = '';
    stdin.on('data', function (chunk) {
      data += chunk;
    });
    stdin.on('end', function () {
      minify(process, options, debugMode, data);
    });
  }
}

function findArgumentTo(option, rawArgs, args) {
  var value = true;
  var optionAt = rawArgs.indexOf(option);
  var nextOption = rawArgs[optionAt + 1];
  var looksLikePath;
  var asArgumentAt;

  if (!nextOption) {
    return value;
  }

  looksLikePath = nextOption.indexOf('.css') > -1 ||
    /\//.test(nextOption) ||
    /\\[^\-]/.test(nextOption) ||
    /^https?:\/\//.test(nextOption);
  asArgumentAt = args.indexOf(nextOption);

  if (!looksLikePath) {
    value = nextOption;
  }

  if (!looksLikePath && asArgumentAt > -1) {
    args.splice(asArgumentAt, 1);
  }

  return value;
}

function minify(process, options, debugMode, data) {
  new CleanCSS(options).minify(data, function (errors, minified) {
    var mapFilename;

    if (debugMode) {
      console.error('Original: %d bytes', minified.stats.originalSize);
      console.error('Minified: %d bytes', minified.stats.minifiedSize);
      console.error('Efficiency: %d%', ~~(minified.stats.efficiency * 10000) / 100.0);
      console.error('Time spent: %dms', minified.stats.timeSpent);

      if (minified.inlinedStylesheets.length > 0) {
        console.error('Inlined stylesheets:');
        minified.inlinedStylesheets.forEach(function (uri) {
          console.error('- %s', uri);
        });
      }
    }

    outputFeedback(minified.errors, true);
    outputFeedback(minified.warnings);

    if (minified.errors.length > 0) {
      process.exit(1);
    }

    if (minified.sourceMap) {
      mapFilename = path.basename(options.output) + '.map';
      output(process, options, minified.styles + '/*# sourceMappingURL=' + mapFilename + ' */');
      outputMap(options, minified.sourceMap, mapFilename);
    } else {
      output(process, options, minified.styles);
    }
  });
}

function outputFeedback(messages, isError) {
  var prefix = isError ? '\x1B[31mERROR\x1B[39m:' : 'WARNING:';

  messages.forEach(function (message) {
    console.error('%s %s', prefix, message);
  });
}

function output(process, options, minified) {
  if (options.output) {
    fs.writeFileSync(options.output, minified, 'utf8');
  } else {
    process.stdout.write(minified);
  }
}

function outputMap(options, sourceMap, mapFilename) {
  var mapPath = path.join(path.dirname(options.output), mapFilename);
  fs.writeFileSync(mapPath, sourceMap.toString(), 'utf-8');
}

module.exports = cli;
<|endoftext|>


<commit_before>'use strict'
const ethUtil = require('ethereumjs-util')
const fees = require('ethereum-common/params.json')
const BN = ethUtil.BN

// secp256k1n/2
const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16)

/**
 * Creates a new transaction object.
 *
 * @example
 * var rawTx = {
 *   nonce: '0x00',
 *   gasPrice: '0x09184e72a000',
 *   gasLimit: '0x2710',
 *   to: '0x0000000000000000000000000000000000000000',
 *   value: '0x00',
 *   data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057',
 *   v: '0x1c',
 *   r: '0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab',
 *   s: '0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13'
 * };
 * var tx = new Transaction(rawTx);
 *
 * @class
 * @param {Buffer | Array | Object} data a transaction can be initiailized with either a buffer containing the RLP serialized transaction or an array of buffers relating to each of the tx Properties, listed in order below in the exmple.
 *
 * Or lastly an Object containing the Properties of the transaction like in the Usage example.
 *
 * For Object and Arrays each of the elements can either be a Buffer, a hex-prefixed (0x) String , Number, or an object with a toBuffer method such as Bignum
 *
 * @property {Buffer} raw The raw rlp encoded transaction
 * @param {Buffer} data.nonce nonce number
 * @param {Buffer} data.gasLimit transaction gas limit
 * @param {Buffer} data.gasPrice transaction gas price
 * @param {Buffer} data.to to the to address
 * @param {Buffer} data.value the amount of ether sent
 * @param {Buffer} data.data this will contain the data of the message or the init of a contract
 * @param {Buffer} data.v EC recovery ID
 * @param {Buffer} data.r EC signature parameter
 * @param {Buffer} data.s EC signature parameter
 * @param {Number} data.chainId EIP 155 chainId - mainnet: 1, ropsten: 3
 * */

class Transaction {
  constructor (data) {
    data = data || {}
    // Define Properties
    const fields = [{
      name: 'nonce',
      length: 32,
      allowLess: true,
      default: new Buffer([])
    }, {
      name: 'gasPrice',
      length: 32,
      allowLess: true,
      default: new Buffer([])
    }, {
      name: 'gasLimit',
      alias: 'gas',
      length: 32,
      allowLess: true,
      default: new Buffer([])
    }, {
      name: 'to',
      allowZero: true,
      length: 20,
      default: new Buffer([])
    }, {
      name: 'value',
      length: 32,
      allowLess: true,
      default: new Buffer([])
    }, {
      name: 'data',
      alias: 'input',
      allowZero: true,
      default: new Buffer([])
    }, {
      name: 'v',
      allowZero: true,
      default: new Buffer([0x1c])
    }, {
      name: 'r',
      length: 32,
      allowZero: true,
      allowLess: true,
      default: new Buffer([])
    }, {
      name: 's',
      length: 32,
      allowZero: true,
      allowLess: true,
      default: new Buffer([])
    }]

    /**
     * Returns the rlp encoding of the transaction
     * @method serialize
     * @return {Buffer}
     * @memberof Transaction
     * @name serialize
     */
    // attached serialize
    ethUtil.defineProperties(this, fields, data)

    /**
     * @property {Buffer} from (read only) sender address of this transaction, mathematically derived from other parameters.
     * @name from
     * @memberof Transaction
     */
    Object.defineProperty(this, 'from', {
      enumerable: true,
      configurable: true,
      get: this.getSenderAddress.bind(this)
    })

    // calculate chainId from signature
    let sigV = ethUtil.bufferToInt(this.v)
    let chainId = Math.floor((sigV - 35) / 2)
    if (chainId < 0) chainId = 0

    // set chainId
    this._chainId = chainId || data.chainId || 0
    this._homestead = true
  }

  /**
   * If the tx's `to` is to the creation address
   * @return {Boolean}
   */
  toCreationAddress () {
    return this.to.toString('hex') === ''
  }

  /**
   * Computes a sha3-256 hash of the serialized tx
   * @param {Boolean} [includeSignature=true] whether or not to inculde the signature
   * @return {Buffer}
   */
  hash (includeSignature) {
    if (includeSignature === undefined) includeSignature = true

    // EIP155 spec:
    // when computing the hash of a transaction for purposes of signing or recovering,
    // instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data),
    // hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0

    let items
    if (includeSignature) {
      items = this.raw
    } else {
      if (this._chainId > 0) {
        const raw = this.raw.slice()
        this.v = this._chainId
        this.r = 0
        this.s = 0
        items = this.raw
        this.raw = raw
      } else {
        items = this.raw.slice(0, 6)
      }
    }

    // create hash
    return ethUtil.rlphash(items)
  }

  /**
   * returns chain ID
   * @return {Buffer}
   */
  getChainId () {
    return this._chainId
  }

  /**
   * returns the sender's address
   * @return {Buffer}
   */
  getSenderAddress () {
    if (this._from) {
      return this._from
    }
    const pubkey = this.getSenderPublicKey()
    this._from = ethUtil.publicToAddress(pubkey)
    return this._from
  }

  /**
   * returns the public key of the sender
   * @return {Buffer}
   */
  getSenderPublicKey () {
    if (!this._senderPubKey || !this._senderPubKey.length) {
      if (!this.verifySignature()) throw new Error('Invalid Signature')
    }
    return this._senderPubKey
  }

  /**
   * Determines if the signature is valid
   * @return {Boolean}
   */
  verifySignature () {
    const msgHash = this.hash(false)
    // All transaction signatures whose s-value is greater than secp256k1n/2 are considered invalid.
    if (this._homestead && new BN(this.s).cmp(N_DIV_2) === 1) {
      return false
    }

    try {
      let v = ethUtil.bufferToInt(this.v)
      if (this._chainId > 0) {
        v -= this._chainId * 2 + 8
      }
      this._senderPubKey = ethUtil.ecrecover(msgHash, v, this.r, this.s)
    } catch (e) {
      return false
    }

    return !!this._senderPubKey
  }

  /**
   * sign a transaction with a given a private key
   * @param {Buffer} privateKey
   */
  sign (privateKey) {
    const msgHash = this.hash(false)
    const sig = ethUtil.ecsign(msgHash, privateKey)
    if (this._chainId > 0) {
      sig.v += this._chainId * 2 + 8
    }
    Object.assign(this, sig)
  }

  /**
   * The amount of gas paid for the data in this tx
   * @return {BN}
   */
  getDataFee () {
    const data = this.raw[5]
    const cost = new BN(0)
    for (let i = 0; i < data.length; i++) {
      data[i] === 0 ? cost.iaddn(fees.txDataZeroGas.v) : cost.iaddn(fees.txDataNonZeroGas.v)
    }
    return cost
  }

  /**
   * the minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee)
   * @return {BN}
   */
  getBaseFee () {
    const fee = this.getDataFee().iaddn(fees.txGas.v)
    if (this._homestead && this.toCreationAddress()) {
      fee.iaddn(fees.txCreation.v)
    }
    return fee
  }

  /**
   * the up front amount that an account must have for this transaction to be valid
   * @return {BN}
   */
  getUpfrontCost () {
    return new BN(this.gasLimit)
      .imul(new BN(this.gasPrice))
      .iadd(new BN(this.value))
  }

  /**
   * validates the signature and checks to see if it has enough gas
   * @param {Boolean} [stringError=false] whether to return a string with a dscription of why the validation failed or return a Bloolean
   * @return {Boolean|String}
   */
  validate (stringError) {
    const errors = []
    if (!this.verifySignature()) {
      errors.push('Invalid Signature')
    }

    if (this.getBaseFee().cmp(new BN(this.gasLimit)) > 0) {
      errors.push([`gas limit is too low. Need at least ${this.getBaseFee()}`])
    }

    if (stringError === undefined || stringError === false) {
      return errors.length === 0
    } else {
      return errors.join(' ')
    }
  }
}

module.exports = Transaction
<commit_msg>Fix typos in jsdocs<commit_after>'use strict'
const ethUtil = require('ethereumjs-util')
const fees = require('ethereum-common/params.json')
const BN = ethUtil.BN

// secp256k1n/2
const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16)

/**
 * Creates a new transaction object.
 *
 * @example
 * var rawTx = {
 *   nonce: '0x00',
 *   gasPrice: '0x09184e72a000',
 *   gasLimit: '0x2710',
 *   to: '0x0000000000000000000000000000000000000000',
 *   value: '0x00',
 *   data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057',
 *   v: '0x1c',
 *   r: '0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab',
 *   s: '0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13'
 * };
 * var tx = new Transaction(rawTx);
 *
 * @class
 * @param {Buffer | Array | Object} data a transaction can be initiailized with either a buffer containing the RLP serialized transaction or an array of buffers relating to each of the tx Properties, listed in order below in the exmple.
 *
 * Or lastly an Object containing the Properties of the transaction like in the Usage example.
 *
 * For Object and Arrays each of the elements can either be a Buffer, a hex-prefixed (0x) String , Number, or an object with a toBuffer method such as Bignum
 *
 * @property {Buffer} raw The raw rlp encoded transaction
 * @param {Buffer} data.nonce nonce number
 * @param {Buffer} data.gasLimit transaction gas limit
 * @param {Buffer} data.gasPrice transaction gas price
 * @param {Buffer} data.to to the to address
 * @param {Buffer} data.value the amount of ether sent
 * @param {Buffer} data.data this will contain the data of the message or the init of a contract
 * @param {Buffer} data.v EC recovery ID
 * @param {Buffer} data.r EC signature parameter
 * @param {Buffer} data.s EC signature parameter
 * @param {Number} data.chainId EIP 155 chainId - mainnet: 1, ropsten: 3
 * */

class Transaction {
  constructor (data) {
    data = data || {}
    // Define Properties
    const fields = [{
      name: 'nonce',
      length: 32,
      allowLess: true,
      default: new Buffer([])
    }, {
      name: 'gasPrice',
      length: 32,
      allowLess: true,
      default: new Buffer([])
    }, {
      name: 'gasLimit',
      alias: 'gas',
      length: 32,
      allowLess: true,
      default: new Buffer([])
    }, {
      name: 'to',
      allowZero: true,
      length: 20,
      default: new Buffer([])
    }, {
      name: 'value',
      length: 32,
      allowLess: true,
      default: new Buffer([])
    }, {
      name: 'data',
      alias: 'input',
      allowZero: true,
      default: new Buffer([])
    }, {
      name: 'v',
      allowZero: true,
      default: new Buffer([0x1c])
    }, {
      name: 'r',
      length: 32,
      allowZero: true,
      allowLess: true,
      default: new Buffer([])
    }, {
      name: 's',
      length: 32,
      allowZero: true,
      allowLess: true,
      default: new Buffer([])
    }]

    /**
     * Returns the rlp encoding of the transaction
     * @method serialize
     * @return {Buffer}
     * @memberof Transaction
     * @name serialize
     */
    // attached serialize
    ethUtil.defineProperties(this, fields, data)

    /**
     * @property {Buffer} from (read only) sender address of this transaction, mathematically derived from other parameters.
     * @name from
     * @memberof Transaction
     */
    Object.defineProperty(this, 'from', {
      enumerable: true,
      configurable: true,
      get: this.getSenderAddress.bind(this)
    })

    // calculate chainId from signature
    let sigV = ethUtil.bufferToInt(this.v)
    let chainId = Math.floor((sigV - 35) / 2)
    if (chainId < 0) chainId = 0

    // set chainId
    this._chainId = chainId || data.chainId || 0
    this._homestead = true
  }

  /**
   * If the tx's `to` is to the creation address
   * @return {Boolean}
   */
  toCreationAddress () {
    return this.to.toString('hex') === ''
  }

  /**
   * Computes a sha3-256 hash of the serialized tx
   * @param {Boolean} [includeSignature=true] whether or not to inculde the signature
   * @return {Buffer}
   */
  hash (includeSignature) {
    if (includeSignature === undefined) includeSignature = true

    // EIP155 spec:
    // when computing the hash of a transaction for purposes of signing or recovering,
    // instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data),
    // hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0

    let items
    if (includeSignature) {
      items = this.raw
    } else {
      if (this._chainId > 0) {
        const raw = this.raw.slice()
        this.v = this._chainId
        this.r = 0
        this.s = 0
        items = this.raw
        this.raw = raw
      } else {
        items = this.raw.slice(0, 6)
      }
    }

    // create hash
    return ethUtil.rlphash(items)
  }

  /**
   * returns chain ID
   * @return {Buffer}
   */
  getChainId () {
    return this._chainId
  }

  /**
   * returns the sender's address
   * @return {Buffer}
   */
  getSenderAddress () {
    if (this._from) {
      return this._from
    }
    const pubkey = this.getSenderPublicKey()
    this._from = ethUtil.publicToAddress(pubkey)
    return this._from
  }

  /**
   * returns the public key of the sender
   * @return {Buffer}
   */
  getSenderPublicKey () {
    if (!this._senderPubKey || !this._senderPubKey.length) {
      if (!this.verifySignature()) throw new Error('Invalid Signature')
    }
    return this._senderPubKey
  }

  /**
   * Determines if the signature is valid
   * @return {Boolean}
   */
  verifySignature () {
    const msgHash = this.hash(false)
    // All transaction signatures whose s-value is greater than secp256k1n/2 are considered invalid.
    if (this._homestead && new BN(this.s).cmp(N_DIV_2) === 1) {
      return false
    }

    try {
      let v = ethUtil.bufferToInt(this.v)
      if (this._chainId > 0) {
        v -= this._chainId * 2 + 8
      }
      this._senderPubKey = ethUtil.ecrecover(msgHash, v, this.r, this.s)
    } catch (e) {
      return false
    }

    return !!this._senderPubKey
  }

  /**
   * sign a transaction with a given private key
   * @param {Buffer} privateKey
   */
  sign (privateKey) {
    const msgHash = this.hash(false)
    const sig = ethUtil.ecsign(msgHash, privateKey)
    if (this._chainId > 0) {
      sig.v += this._chainId * 2 + 8
    }
    Object.assign(this, sig)
  }

  /**
   * The amount of gas paid for the data in this tx
   * @return {BN}
   */
  getDataFee () {
    const data = this.raw[5]
    const cost = new BN(0)
    for (let i = 0; i < data.length; i++) {
      data[i] === 0 ? cost.iaddn(fees.txDataZeroGas.v) : cost.iaddn(fees.txDataNonZeroGas.v)
    }
    return cost
  }

  /**
   * the minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee)
   * @return {BN}
   */
  getBaseFee () {
    const fee = this.getDataFee().iaddn(fees.txGas.v)
    if (this._homestead && this.toCreationAddress()) {
      fee.iaddn(fees.txCreation.v)
    }
    return fee
  }

  /**
   * the up front amount that an account must have for this transaction to be valid
   * @return {BN}
   */
  getUpfrontCost () {
    return new BN(this.gasLimit)
      .imul(new BN(this.gasPrice))
      .iadd(new BN(this.value))
  }

  /**
   * validates the signature and checks to see if it has enough gas
   * @param {Boolean} [stringError=false] whether to return a string with a description of why the validation failed or return a Boolean
   * @return {Boolean|String}
   */
  validate (stringError) {
    const errors = []
    if (!this.verifySignature()) {
      errors.push('Invalid Signature')
    }

    if (this.getBaseFee().cmp(new BN(this.gasLimit)) > 0) {
      errors.push([`gas limit is too low. Need at least ${this.getBaseFee()}`])
    }

    if (stringError === undefined || stringError === false) {
      return errors.length === 0
    } else {
      return errors.join(' ')
    }
  }
}

module.exports = Transaction
<|endoftext|>


<commit_before>"use strict";

var AWS = require('aws-sdk');

console.log("AWS Lambda SES Forwarder // @arithmetric // Version 4.0.0");

// Configure the S3 bucket and key prefix for stored raw emails, and the
// mapping of email addresses to forward from and to.
//
// Expected keys/values:
//
// - fromEmail: Forwarded emails will come from this verified address
//
// - subjectPrefix: Forwarded emails subject will contain this prefix
//
// - emailBucket: S3 bucket name where SES stores emails.
//
// - emailKeyPrefix: S3 key name prefix where SES stores email. Include the
//   trailing slash.
//
// - forwardMapping: Object where the key is the lowercase email address from
//   which to forward and the value is an array of email addresses to which to
//   send the message.
//
//   To match all email addresses on a domain, use a key without the name part
//   of an email address before the "at" symbol (i.e. `@example.com`).
//
//   To match a mailbox name on all domains, use a key without the "at" symbol
//   and domain part of an email address (i.e. `info`).
var defaultConfig = {
  fromEmail: "noreply@example.com",
  subjectPrefix: "",
  emailBucket: "s3-bucket-name",
  emailKeyPrefix: "emailsPrefix/",
  forwardMapping: {
    "info@example.com": [
      "example.john@example.com",
      "example.jen@example.com"
    ],
    "abuse@example.com": [
      "example.jim@example.com"
    ],
    "@example.com": [
      "example.john@example.com"
    ],
    "info": [
      "info@example.com"
    ]
  }
};

/**
 * Parses the SES event record provided for the `mail` and `receipients` data.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
exports.parseEvent = function(data) {
  // Validate characteristics of a SES event record.
  if (!data.event ||
      !data.event.hasOwnProperty('Records') ||
      data.event.Records.length !== 1 ||
      !data.event.Records[0].hasOwnProperty('eventSource') ||
      data.event.Records[0].eventSource !== 'aws:ses' ||
      data.event.Records[0].eventVersion !== '1.0') {
    data.log({message: "parseEvent() received invalid SES message:",
      level: "error", event: JSON.stringify(data.event)});
    return Promise.reject(new Error('Error: Received invalid SES message.'));
  }

  data.email = data.event.Records[0].ses.mail;
  data.recipients = data.event.Records[0].ses.receipt.recipients;
  return Promise.resolve(data);
};

/**
 * Transforms the original recipients to the desired forwarded destinations.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
exports.transformRecipients = function(data) {
  var newRecipients = [];
  data.originalRecipients = data.recipients;
  data.recipients.forEach(function(origEmail) {
    var origEmailKey = origEmail.toLowerCase();
    if (data.config.forwardMapping.hasOwnProperty(origEmailKey)) {
      newRecipients = newRecipients.concat(
        data.config.forwardMapping[origEmailKey]);
      data.originalRecipient = origEmail;
    } else {
      var origEmailDomain;
      var origEmailUser;
      var pos = origEmailKey.lastIndexOf("@");
      if (pos === -1) {
        origEmailUser = origEmailKey;
      } else {
        origEmailDomain = origEmailKey.slice(pos);
        origEmailUser = origEmailKey.slice(0, pos);
      }
      if (origEmailDomain &&
          data.config.forwardMapping.hasOwnProperty(origEmailDomain)) {
        newRecipients = newRecipients.concat(
          data.config.forwardMapping[origEmailDomain]);
        data.originalRecipient = origEmail;
      } else if (origEmailUser &&
        data.config.forwardMapping.hasOwnProperty(origEmailUser)) {
        newRecipients = newRecipients.concat(
          data.config.forwardMapping[origEmailUser]);
        data.originalRecipient = origEmail;
      }
    }
  });

  if (!newRecipients.length) {
    data.log({message: "Finishing process. No new recipients found for " +
      "original destinations: " + data.originalRecipients.join(", "),
      level: "info"});
    return data.callback();
  }

  data.recipients = newRecipients;
  return Promise.resolve(data);
};

/**
 * Fetches the message data from S3.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
exports.fetchMessage = function(data) {
  // Copying email object to ensure read permission
  data.log({level: "info", message: "Fetching email at s3://" +
    data.config.emailBucket + '/' + data.config.emailKeyPrefix +
    data.email.messageId});
  return new Promise(function(resolve, reject) {
    data.s3.copyObject({
      Bucket: data.config.emailBucket,
      CopySource: data.config.emailBucket + '/' + data.config.emailKeyPrefix +
        data.email.messageId,
      Key: data.config.emailKeyPrefix + data.email.messageId,
      ACL: 'private',
      ContentType: 'text/plain',
      StorageClass: 'STANDARD'
    }, function(err) {
      if (err) {
        data.log({level: "error", message: "copyObject() returned error:",
          error: err, stack: err.stack});
        return reject(
          new Error("Error: Could not make readable copy of email."));
      }

      // Load the raw email from S3
      data.s3.getObject({
        Bucket: data.config.emailBucket,
        Key: data.config.emailKeyPrefix + data.email.messageId
      }, function(err, result) {
        if (err) {
          data.log({level: "error", message: "getObject() returned error:",
            error: err, stack: err.stack});
          return reject(
            new Error("Error: Failed to load message body from S3."));
        }
        data.emailData = result.Body.toString();
        return resolve(data);
      });
    });
  });
};

/**
 * Processes the message data, making updates to recipients and other headers
 * before forwarding message.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
exports.processMessage = function(data) {
  var match = data.emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
  var header = match && match[1] ? match[1] : data.emailData;
  var body = match && match[2] ? match[2] : '';

  // Add "Reply-To:" with the "From" address if it doesn't already exists
  if (!/^Reply-To: /mi.test(header)) {
    match = header.match(/^From: (.*\r?\n)/m);
    var from = match && match[1] ? match[1] : '';
    if (from) {
      header = header + 'Reply-To: ' + from;
      data.log({level: "info", message: "Added Reply-To address of: " + from});
    } else {
      data.log({level: "info", message: "Reply-To address not added because " +
       "From address was not properly extracted."});
    }
  }

  // SES does not allow sending messages from an unverified address,
  // so replace the message's "From:" header with the original
  // recipient (which is a verified domain)
  header = header.replace(
    /^From: (.*)/mg,
    function(match, from) {
      var fromText;
      if (data.config.fromEmail) {
        fromText = 'From: ' + from.replace(/<(.*)>/, '').trim() +
        ' <' + data.config.fromEmail + '>';
      } else {
        fromText = 'From: ' + from.replace('<', 'at ').replace('>', '') +
        ' <' + data.originalRecipient + '>';
      }
      return fromText;
    });

  // Add a prefix to the Subject
  if (data.config.subjectPrefix) {
    header = header.replace(
      /^Subject: (.*)/mg,
      function(match, subject) {
        return 'Subject: ' + data.config.subjectPrefix + subject;
      });
  }

  // Remove the Return-Path header.
  header = header.replace(/^Return-Path: (.*)\r?\n/mg, '');

  // Remove Sender header.
  header = header.replace(/^Sender: (.*)\r?\n/mg, '');

  // Remove all DKIM-Signature headers to prevent triggering an
  // "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
  // These signatures will likely be invalid anyways, since the From
  // header was modified.
  header = header.replace(/^DKIM-Signature: .*\r?\n(\s+.*\r?\n)*/mg, '');

  data.emailData = header + body;
  return Promise.resolve(data);
};

/**
 * Send email using the SES sendRawEmail command.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
exports.sendMessage = function(data) {
  var params = {
    Destinations: data.recipients,
    Source: data.originalRecipient,
    RawMessage: {
      Data: data.emailData
    }
  };
  data.log({level: "info", message: "sendMessage: Sending email via SES. " +
    "Original recipients: " + data.originalRecipients.join(", ") +
    ". Transformed recipients: " + data.recipients.join(", ") + "."});
  return new Promise(function(resolve, reject) {
    data.ses.sendRawEmail(params, function(err, result) {
      if (err) {
        data.log({level: "error", message: "sendRawEmail() returned error.",
          error: err, stack: err.stack});
        return reject(new Error('Error: Email sending failed.'));
      }
      data.log({level: "info", message: "sendRawEmail() successful.",
        result: result});
      resolve(data);
    });
  });
};

/**
 * Handler function to be invoked by AWS Lambda with an inbound SES email as
 * the event.
 *
 * @param {object} event - Lambda event from inbound email received by AWS SES.
 * @param {object} context - Lambda context object.
 * @param {object} callback - Lambda callback object.
 * @param {object} overrides - Overrides for the default data, including the
 * configuration, SES object, and S3 object.
 */
exports.handler = function(event, context, callback, overrides) {
  var steps = overrides && overrides.steps ? overrides.steps :
  [
    exports.parseEvent,
    exports.transformRecipients,
    exports.fetchMessage,
    exports.processMessage,
    exports.sendMessage
  ];
  var data = {
    event: event,
    callback: callback,
    context: context,
    config: overrides && overrides.config ? overrides.config : defaultConfig,
    log: overrides && overrides.log ? overrides.log : console.log,
    ses: overrides && overrides.ses ? overrides.ses : new AWS.SES(),
    s3: overrides && overrides.s3 ? overrides.s3 : new AWS.S3()
  };
  Promise.series(steps, data)
    .then(function(data) {
      data.log({level: "info", message: "Process finished successfully."});
      return data.callback();
    })
    .catch(function(err) {
      data.log({level: "error", message: "Step returned error: " + err.message,
        error: err, stack: err.stack});
      return data.callback(new Error("Error: Step returned error."));
    });
};

Promise.series = function(promises, initValue) {
  return promises.reduce(function(chain, promise) {
    if (typeof promise !== 'function') {
      return Promise.reject(new Error("Error: Invalid promise item: " +
        promise));
    }
    return chain.then(promise);
  }, Promise.resolve(initValue));
};
<commit_msg>Changed AWS.S3() implementation to use signature version v4 for compatibility with S3 EU-based regions.<commit_after>"use strict";

var AWS = require('aws-sdk');

console.log("AWS Lambda SES Forwarder // @arithmetric // Version 4.0.0");

// Configure the S3 bucket and key prefix for stored raw emails, and the
// mapping of email addresses to forward from and to.
//
// Expected keys/values:
//
// - fromEmail: Forwarded emails will come from this verified address
//
// - subjectPrefix: Forwarded emails subject will contain this prefix
//
// - emailBucket: S3 bucket name where SES stores emails.
//
// - emailKeyPrefix: S3 key name prefix where SES stores email. Include the
//   trailing slash.
//
// - forwardMapping: Object where the key is the lowercase email address from
//   which to forward and the value is an array of email addresses to which to
//   send the message.
//
//   To match all email addresses on a domain, use a key without the name part
//   of an email address before the "at" symbol (i.e. `@example.com`).
//
//   To match a mailbox name on all domains, use a key without the "at" symbol
//   and domain part of an email address (i.e. `info`).
var defaultConfig = {
  fromEmail: "noreply@example.com",
  subjectPrefix: "",
  emailBucket: "s3-bucket-name",
  emailKeyPrefix: "emailsPrefix/",
  forwardMapping: {
    "info@example.com": [
      "example.john@example.com",
      "example.jen@example.com"
    ],
    "abuse@example.com": [
      "example.jim@example.com"
    ],
    "@example.com": [
      "example.john@example.com"
    ],
    "info": [
      "info@example.com"
    ]
  }
};

/**
 * Parses the SES event record provided for the `mail` and `receipients` data.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
exports.parseEvent = function(data) {
  // Validate characteristics of a SES event record.
  if (!data.event ||
      !data.event.hasOwnProperty('Records') ||
      data.event.Records.length !== 1 ||
      !data.event.Records[0].hasOwnProperty('eventSource') ||
      data.event.Records[0].eventSource !== 'aws:ses' ||
      data.event.Records[0].eventVersion !== '1.0') {
    data.log({message: "parseEvent() received invalid SES message:",
      level: "error", event: JSON.stringify(data.event)});
    return Promise.reject(new Error('Error: Received invalid SES message.'));
  }

  data.email = data.event.Records[0].ses.mail;
  data.recipients = data.event.Records[0].ses.receipt.recipients;
  return Promise.resolve(data);
};

/**
 * Transforms the original recipients to the desired forwarded destinations.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
exports.transformRecipients = function(data) {
  var newRecipients = [];
  data.originalRecipients = data.recipients;
  data.recipients.forEach(function(origEmail) {
    var origEmailKey = origEmail.toLowerCase();
    if (data.config.forwardMapping.hasOwnProperty(origEmailKey)) {
      newRecipients = newRecipients.concat(
        data.config.forwardMapping[origEmailKey]);
      data.originalRecipient = origEmail;
    } else {
      var origEmailDomain;
      var origEmailUser;
      var pos = origEmailKey.lastIndexOf("@");
      if (pos === -1) {
        origEmailUser = origEmailKey;
      } else {
        origEmailDomain = origEmailKey.slice(pos);
        origEmailUser = origEmailKey.slice(0, pos);
      }
      if (origEmailDomain &&
          data.config.forwardMapping.hasOwnProperty(origEmailDomain)) {
        newRecipients = newRecipients.concat(
          data.config.forwardMapping[origEmailDomain]);
        data.originalRecipient = origEmail;
      } else if (origEmailUser &&
        data.config.forwardMapping.hasOwnProperty(origEmailUser)) {
        newRecipients = newRecipients.concat(
          data.config.forwardMapping[origEmailUser]);
        data.originalRecipient = origEmail;
      }
    }
  });

  if (!newRecipients.length) {
    data.log({message: "Finishing process. No new recipients found for " +
      "original destinations: " + data.originalRecipients.join(", "),
      level: "info"});
    return data.callback();
  }

  data.recipients = newRecipients;
  return Promise.resolve(data);
};

/**
 * Fetches the message data from S3.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
exports.fetchMessage = function(data) {
  // Copying email object to ensure read permission
  data.log({level: "info", message: "Fetching email at s3://" +
    data.config.emailBucket + '/' + data.config.emailKeyPrefix +
    data.email.messageId});
  return new Promise(function(resolve, reject) {
    data.s3.copyObject({
      Bucket: data.config.emailBucket,
      CopySource: data.config.emailBucket + '/' + data.config.emailKeyPrefix +
        data.email.messageId,
      Key: data.config.emailKeyPrefix + data.email.messageId,
      ACL: 'private',
      ContentType: 'text/plain',
      StorageClass: 'STANDARD'
    }, function(err) {
      if (err) {
        data.log({level: "error", message: "copyObject() returned error:",
          error: err, stack: err.stack});
        return reject(
          new Error("Error: Could not make readable copy of email."));
      }

      // Load the raw email from S3
      data.s3.getObject({
        Bucket: data.config.emailBucket,
        Key: data.config.emailKeyPrefix + data.email.messageId
      }, function(err, result) {
        if (err) {
          data.log({level: "error", message: "getObject() returned error:",
            error: err, stack: err.stack});
          return reject(
            new Error("Error: Failed to load message body from S3."));
        }
        data.emailData = result.Body.toString();
        return resolve(data);
      });
    });
  });
};

/**
 * Processes the message data, making updates to recipients and other headers
 * before forwarding message.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
exports.processMessage = function(data) {
  var match = data.emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
  var header = match && match[1] ? match[1] : data.emailData;
  var body = match && match[2] ? match[2] : '';

  // Add "Reply-To:" with the "From" address if it doesn't already exists
  if (!/^Reply-To: /mi.test(header)) {
    match = header.match(/^From: (.*\r?\n)/m);
    var from = match && match[1] ? match[1] : '';
    if (from) {
      header = header + 'Reply-To: ' + from;
      data.log({level: "info", message: "Added Reply-To address of: " + from});
    } else {
      data.log({level: "info", message: "Reply-To address not added because " +
       "From address was not properly extracted."});
    }
  }

  // SES does not allow sending messages from an unverified address,
  // so replace the message's "From:" header with the original
  // recipient (which is a verified domain)
  header = header.replace(
    /^From: (.*)/mg,
    function(match, from) {
      var fromText;
      if (data.config.fromEmail) {
        fromText = 'From: ' + from.replace(/<(.*)>/, '').trim() +
        ' <' + data.config.fromEmail + '>';
      } else {
        fromText = 'From: ' + from.replace('<', 'at ').replace('>', '') +
        ' <' + data.originalRecipient + '>';
      }
      return fromText;
    });

  // Add a prefix to the Subject
  if (data.config.subjectPrefix) {
    header = header.replace(
      /^Subject: (.*)/mg,
      function(match, subject) {
        return 'Subject: ' + data.config.subjectPrefix + subject;
      });
  }

  // Remove the Return-Path header.
  header = header.replace(/^Return-Path: (.*)\r?\n/mg, '');

  // Remove Sender header.
  header = header.replace(/^Sender: (.*)\r?\n/mg, '');

  // Remove all DKIM-Signature headers to prevent triggering an
  // "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
  // These signatures will likely be invalid anyways, since the From
  // header was modified.
  header = header.replace(/^DKIM-Signature: .*\r?\n(\s+.*\r?\n)*/mg, '');

  data.emailData = header + body;
  return Promise.resolve(data);
};

/**
 * Send email using the SES sendRawEmail command.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
exports.sendMessage = function(data) {
  var params = {
    Destinations: data.recipients,
    Source: data.originalRecipient,
    RawMessage: {
      Data: data.emailData
    }
  };
  data.log({level: "info", message: "sendMessage: Sending email via SES. " +
    "Original recipients: " + data.originalRecipients.join(", ") +
    ". Transformed recipients: " + data.recipients.join(", ") + "."});
  return new Promise(function(resolve, reject) {
    data.ses.sendRawEmail(params, function(err, result) {
      if (err) {
        data.log({level: "error", message: "sendRawEmail() returned error.",
          error: err, stack: err.stack});
        return reject(new Error('Error: Email sending failed.'));
      }
      data.log({level: "info", message: "sendRawEmail() successful.",
        result: result});
      resolve(data);
    });
  });
};

/**
 * Handler function to be invoked by AWS Lambda with an inbound SES email as
 * the event.
 *
 * @param {object} event - Lambda event from inbound email received by AWS SES.
 * @param {object} context - Lambda context object.
 * @param {object} callback - Lambda callback object.
 * @param {object} overrides - Overrides for the default data, including the
 * configuration, SES object, and S3 object.
 */
exports.handler = function(event, context, callback, overrides) {
  var steps = overrides && overrides.steps ? overrides.steps :
  [
    exports.parseEvent,
    exports.transformRecipients,
    exports.fetchMessage,
    exports.processMessage,
    exports.sendMessage
  ];
  var data = {
    event: event,
    callback: callback,
    context: context,
    config: overrides && overrides.config ? overrides.config : defaultConfig,
    log: overrides && overrides.log ? overrides.log : console.log,
    ses: overrides && overrides.ses ? overrides.ses : new AWS.SES(),
    s3: overrides && overrides.s3 ?
      overrides.s3 : new AWS.S3({signatureVersion: 'v4'})
  };
  Promise.series(steps, data)
    .then(function(data) {
      data.log({level: "info", message: "Process finished successfully."});
      return data.callback();
    })
    .catch(function(err) {
      data.log({level: "error", message: "Step returned error: " + err.message,
        error: err, stack: err.stack});
      return data.callback(new Error("Error: Step returned error."));
    });
};

Promise.series = function(promises, initValue) {
  return promises.reduce(function(chain, promise) {
    if (typeof promise !== 'function') {
      return Promise.reject(new Error("Error: Invalid promise item: " +
        promise));
    }
    return chain.then(promise);
  }, Promise.resolve(initValue));
};
<|endoftext|>


<commit_before>/* global AFRAME */

if (typeof AFRAME === 'undefined') {
  throw new Error('Component attempted to register before AFRAME was available.');
}

/**
 * Gaze Control component for A-Frame.
 */
AFRAME.registerComponent('gaze-control', {

  schema: {
    enabled: {default: true},
    widthSectors: {type: 'number', default: 3},
    heightSectors: {type: 'number', default: 3}
  },

  /**
   * Set if component needs multiple instancing.
   */
  multiple: false,

  /**
   * Called once when component is attached. Generally for initial setup.
   */
  init: function () {
    this.previousPosition = new THREE.Vector3();
    this.deltaPosition = new THREE.Vector3();
  },

  /**
   * Called when component is attached and when component data changes.
   * Generally modifies the entity based on the data.
   */
  update: function (oldData) {
    if (!this.data.enabled) { return; }
  },

  /**
   * Called when a component is removed (e.g., via removeAttribute).
   * Generally undoes all modifications to the entity.
   */
  remove: function () {
    this.pause();
  },

  /**
   * Called on each scene tick.
   */
  // tick: function (t) { },

  /**
   * Called when entity pauses.
   * Use to stop or remove any dynamic or background behavior such as events.
   */
  pause: function () {
    var sceneEl = this.el.sceneEl;
    var canvasEl = sceneEl.canvas;

    canvasEl.removeEventListener('gazemove', this.onGazeMove);
  },

  /**
   * Called when entity resumes.
   * Use to continue or add any dynamic or background behavior such as events.
   */
  play: function () {
    var sceneEl = this.el.sceneEl;
    var canvasEl = sceneEl.canvas;

    // listen for canvas to load.
    if (!canvasEl) {
      sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this));
      return;
    }

    this.previousPosition.set(0, 0, 0);
    canvasEl.addEventListener('gazemove', this.onGazeMove);
  },

  onGazeMove: function(event) {
    // Base sector size and movements on the size of the window
    var width = window.innerWidth;
    var height = window.innerHeight;

    // Because of landscape orientation and focal area, a larger portion
    // of the height contributes to a sector size than width.
    var sector_height = height/4; // height dimension of sector
    var sector_width = width/6; // width dimension of sector

    // Speed is somewhat arbitrary but obtained based on what felt
    // comfortable in testing.
    var movement_speed = 25; // speed of panning

    // Adapted from onMouseMove()
    var pitchObject = this.pitchObject;
    var yawObject = this.yawObject;
    var previousGazeEvent = this.previousGazeEvent;
    if (!this.data.enabled) { return; }

    // Bottom Left
    if (event.detail.x > 0 && event.detail.x < sector_width && event.detail.y > 0 && event.detail.y < sector_height){
      var movementX = -movement_speed;
      var movementY = -movement_speed;
    } // Bottom Center
    else if (event.detail.x > sector_width && event.detail.x < width - sector_width && event.detail.y > 0 && event.detail.y < sector_height){
      var movementX = 0;
      var movementY = -movement_speed;
    } // Bottom Right
    else if (event.detail.x > width - sector_width && event.detail.x < width && event.detail.y > 0 && event.detail.y < sector_height){
      var movementX = movement_speed;
      var movementY = -movement_speed;
    } // Middle Left
    else if (event.detail.x > 0 && event.detail.x < sector_width && event.detail.y > sector_height && event.detail.y < height - sector_height){
      var movementX = -movement_speed;
      var movementY = 0;
    } // Middle Right
    else if (event.detail.x > width - sector_width && event.detail.x < width && event.detail.y > sector_height && event.detail.y < height - sector_height){
      var movementX = movement_speed;
      var movementY = 0;
    } // Top Left
    else if (event.detail.x > 0 && event.detail.x < sector_width && event.detail.y > height - sector_height && event.detail.y < height){
      var movementX = -movement_speed
      var movementY = movement_speed;
    } // Top Center
    else if (event.detail.x > sector_width && event.detail.x < width - sector_width && event.detail.y > height - sector_height && event.detail.y < height){
      var movementX = 0;
      var movementY = movement_speed;
    } // Top Right
    else if (event.detail.x > width - sector_width && event.detail.x < width && event.detail.y > height - sector_height && event.detail.y < height){
      var movementX = movement_speed;
      var movementY = movement_speed;
    } // User is looking at center or not at another sector
    else{
      var movementX = 0;
      var movementY = 0;
    }

    // Adapted from onMouseMove()
    this.previousGazeEvent = event;
    yawObject.rotation.y -= movementX * 0.002;
    pitchObject.rotation.x -= movementY * 0.002;
    pitchObject.rotation.x = Math.max(-PI_2, Math.min(PI_2, pitchObject.rotation.x));
  },
});
<commit_msg>Added rest of proof-of-concept code to component.<commit_after>// To avoid recalculation at every mouse movement tick
var PI_2 = Math.PI / 2;

/* global AFRAME */

if (typeof AFRAME === 'undefined') {
  throw new Error('Component attempted to register before AFRAME was available.');
}

/**
 * Gaze Control component for A-Frame.
 */
AFRAME.registerComponent('gaze-control', {
   dependencies: ['position', 'rotation'],

   schema: {
     enabled: { default: true }
   },

   init: function () {
     this.previousPosition = new THREE.Vector3();
     this.deltaPosition = new THREE.Vector3();
     this.setupMouseControls();
     this.setupHMDControls();
     this.bindMethods();
   },

   update: function () {
     if (!this.data.enabled) { return; }
     this.controls.update();
     this.updateOrientation();
     this.updatePosition();
   },

   play: function () {
     this.previousPosition.set(0, 0, 0);
     this.addEventListeners();
   },

   pause: function () {
     this.removeEventListeners();
   },

   tick: function (t) {
     this.update();
   },

   remove: function () {
     this.pause();
   },

   bindMethods: function () {
     this.onMouseDown = this.onMouseDown.bind(this);
     this.onGazeMove = this.onGazeMove.bind(this);
     this.onMouseMove = this.onMouseMove.bind(this);
     this.releaseMouse = this.releaseMouse.bind(this);
     this.onTouchStart = this.onTouchStart.bind(this);
     this.onTouchMove = this.onTouchMove.bind(this);
     this.onTouchEnd = this.onTouchEnd.bind(this);
   },

   setupMouseControls: function () {
     // The canvas where the scene is painted
     this.mouseDown = false;
     this.pitchObject = new THREE.Object3D();
     this.yawObject = new THREE.Object3D();
     this.yawObject.position.y = 10;
     this.yawObject.add(this.pitchObject);
   },

   setupHMDControls: function () {
     this.dolly = new THREE.Object3D();
     this.euler = new THREE.Euler();
     this.controls = new THREE.VRControls(this.dolly);
     this.zeroQuaternion = new THREE.Quaternion();
   },

   addEventListeners: function () {
     var sceneEl = this.el.sceneEl;
     var canvasEl = sceneEl.canvas;

     // listen for canvas to load.
     if (!canvasEl) {
       sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this));
       return;
     }

     // Mouse Events
     canvasEl.addEventListener('mousedown', this.onMouseDown, false);
     canvasEl.addEventListener('mousemove', this.onMouseMove, false);
     canvasEl.addEventListener('mouseup', this.releaseMouse, false);
     canvasEl.addEventListener('mouseout', this.releaseMouse, false);

     // Touch events
     canvasEl.addEventListener('touchstart', this.onTouchStart);
     canvasEl.addEventListener('touchmove', this.onTouchMove);
     canvasEl.addEventListener('touchend', this.onTouchEnd);

     // Gaze events
     canvasEl.addEventListener('gazemove', this.onGazeMove);
   },

   removeEventListeners: function () {
     var sceneEl = document.querySelector('a-scene');
     var canvasEl = sceneEl && sceneEl.canvas;
     if (!canvasEl) { return; }

     // Mouse Events
     canvasEl.removeEventListener('mousedown', this.onMouseDown);
     canvasEl.removeEventListener('mousemove', this.onMouseMove);
     canvasEl.removeEventListener('mouseup', this.releaseMouse);
     canvasEl.removeEventListener('mouseout', this.releaseMouse);

     // Touch events
     canvasEl.removeEventListener('touchstart', this.onTouchStart);
     canvasEl.removeEventListener('touchmove', this.onTouchMove);
     canvasEl.removeEventListener('touchend', this.onTouchEnd);

     // Gaze events
     canvasEl.removeEventListener('gazemove', this.onGazeMove);
   },

   updateOrientation: (function () {
     var hmdEuler = new THREE.Euler();
     hmdEuler.order = 'YXZ';
     return function () {
       var pitchObject = this.pitchObject;
       var yawObject = this.yawObject;
       var hmdQuaternion = this.calculateHMDQuaternion();
       hmdEuler.setFromQuaternion(hmdQuaternion);
       this.el.setAttribute('rotation', {
         x: THREE.Math.radToDeg(hmdEuler.x) + THREE.Math.radToDeg(pitchObject.rotation.x),
         y: THREE.Math.radToDeg(hmdEuler.y) + THREE.Math.radToDeg(yawObject.rotation.y),
         z: THREE.Math.radToDeg(hmdEuler.z)
       });
     };
   })(),

   calculateHMDQuaternion: (function () {
     var hmdQuaternion = new THREE.Quaternion();
     return function () {
       var dolly = this.dolly;
       if (!this.zeroed && !dolly.quaternion.equals(this.zeroQuaternion)) {
         this.zeroOrientation();
         this.zeroed = true;
       }
       hmdQuaternion.copy(this.zeroQuaternion).multiply(dolly.quaternion);
       return hmdQuaternion;
     };
   })(),

   updatePosition: function () {
     var el = this.el;
     var deltaPosition = this.calculateDeltaPosition();
     var currentPosition = el.getAttribute('position');
     el.setAttribute('position', {
       x: currentPosition.x + deltaPosition.x,
       y: currentPosition.y + deltaPosition.y,
       z: currentPosition.z + deltaPosition.z
     });
   },

   calculateDeltaPosition: function () {
     var dolly = this.dolly;
     var deltaPosition = this.deltaPosition;
     var previousPosition = this.previousPosition;
     deltaPosition.copy(dolly.position);
     deltaPosition.sub(previousPosition);
     previousPosition.copy(dolly.position);
     return deltaPosition;
   },

   updateHMDQuaternion: (function () {
     var hmdQuaternion = new THREE.Quaternion();
     return function () {
       var dolly = this.dolly;
       this.controls.update();
       if (!this.zeroed && !dolly.quaternion.equals(this.zeroQuaternion)) {
         this.zeroOrientation();
         this.zeroed = true;
       }
       hmdQuaternion.copy(this.zeroQuaternion).multiply(dolly.quaternion);
       return hmdQuaternion;
     };
   })(),

   zeroOrientation: function () {
     var euler = new THREE.Euler();
     euler.setFromQuaternion(this.dolly.quaternion.clone().inverse());
     // Cancel out roll and pitch. We want to only reset yaw
     euler.z = 0;
     euler.x = 0;
     this.zeroQuaternion.setFromEuler(euler);
   },

   // Listener for gaze events that manipulates the scene based on gaze
   // location in window. Window is broken into 9 sectors in a 3x3 grid.
   // The center sector is the largest because the focal area consumes
   // most of the screen.
   onGazeMove: function(event) {
     // Base sector size and movements on the size of the window
     var width = window.innerWidth;
     var height = window.innerHeight;

     // Because of landscape orientation and focal area, a larger portion
     // of the height contributes to a sector size than width.
     var sector_height = height/4; // height dimension of sector
     var sector_width = width/6; // width dimension of sector

     // Speed is somewhat arbitrary but obtained based on what felt
     // comfortable in testing.
     var movement_speed = 25; // speed of panning

     // Adapted from onMouseMove()
     var pitchObject = this.pitchObject;
     var yawObject = this.yawObject;
     var previousGazeEvent = this.previousGazeEvent;
     if (!this.data.enabled) { return; }

     // Bottom Left
     if (event.detail.x > 0 && event.detail.x < sector_width && event.detail.y > 0 && event.detail.y < sector_height){
       var movementX = -movement_speed;
       var movementY = -movement_speed;
     } // Bottom Center
     else if (event.detail.x > sector_width && event.detail.x < width - sector_width && event.detail.y > 0 && event.detail.y < sector_height){
       var movementX = 0;
       var movementY = -movement_speed;
     } // Bottom Right
     else if (event.detail.x > width - sector_width && event.detail.x < width && event.detail.y > 0 && event.detail.y < sector_height){
       var movementX = movement_speed;
       var movementY = -movement_speed;
     } // Middle Left
     else if (event.detail.x > 0 && event.detail.x < sector_width && event.detail.y > sector_height && event.detail.y < height - sector_height){
       var movementX = -movement_speed;
       var movementY = 0;
     } // Middle Right
     else if (event.detail.x > width - sector_width && event.detail.x < width && event.detail.y > sector_height && event.detail.y < height - sector_height){
       var movementX = movement_speed;
       var movementY = 0;
     } // Top Left
     else if (event.detail.x > 0 && event.detail.x < sector_width && event.detail.y > height - sector_height && event.detail.y < height){
       var movementX = -movement_speed
       var movementY = movement_speed;
     } // Top Center
     else if (event.detail.x > sector_width && event.detail.x < width - sector_width && event.detail.y > height - sector_height && event.detail.y < height){
       var movementX = 0;
       var movementY = movement_speed;
     } // Top Right
     else if (event.detail.x > width - sector_width && event.detail.x < width && event.detail.y > height - sector_height && event.detail.y < height){
       var movementX = movement_speed;
       var movementY = movement_speed;
     } // User is looking at center or not at another sector
     else{
       console.log('middle');
       var movementX = 0;
       var movementY = 0;
     }

     // Adapted from onMouseMove()
     this.previousGazeEvent = event;
     yawObject.rotation.y -= movementX * 0.002;
     pitchObject.rotation.x -= movementY * 0.002;
     pitchObject.rotation.x = Math.max(-PI_2, Math.min(PI_2, pitchObject.rotation.x));
   },

   onMouseMove: function (event) {
     var pitchObject = this.pitchObject;
     var yawObject = this.yawObject;
     var previousMouseEvent = this.previousMouseEvent;

     if (!this.mouseDown || !this.data.enabled) { return; }

     var movementX = event.movementX || event.mozMovementX;
     var movementY = event.movementY || event.mozMovementY;

     if (movementX === undefined || movementY === undefined) {
       movementX = event.screenX - previousMouseEvent.screenX;
       movementY = event.screenY - previousMouseEvent.screenY;
     }
     this.previousMouseEvent = event;

     yawObject.rotation.y -= movementX * 0.002;
     pitchObject.rotation.x -= movementY * 0.002;
     pitchObject.rotation.x = Math.max(-PI_2, Math.min(PI_2, pitchObject.rotation.x));
   },

   onMouseDown: function (event) {
     this.mouseDown = true;
     this.previousMouseEvent = event;
   },

   releaseMouse: function () {
     this.mouseDown = false;
   },

   onTouchStart: function (e) {
     if (e.touches.length !== 1) { return; }
     this.touchStart = {
       x: e.touches[0].pageX,
       y: e.touches[0].pageY
     };
     this.touchStarted = true;
   },

   onTouchMove: function (e) {
     var deltaY;
     var yawObject = this.yawObject;
     if (!this.touchStarted) { return; }
     deltaY = 2 * Math.PI * (e.touches[0].pageX - this.touchStart.x) /
             this.el.sceneEl.canvas.clientWidth;
     // Limits touch orientaion to to yaw (y axis)
     yawObject.rotation.y -= deltaY * 0.5;
     this.touchStart = {
       x: e.touches[0].pageX,
       y: e.touches[0].pageY
     };
   },

   onTouchEnd: function () {
     this.touchStarted = false;
   }
 });
<|endoftext|>