encharm/http-master

View on GitHub
src/certScanner.js

Summary

Maintainability
D
2 days
Test Coverage
'use strict';

var x509 = require('x509.js');
var fs = require('fs');
var async = require('async');
var path = require('path');
var moment = require('moment');
var util = require('util');

var EventEmitter = require('eventemitter3');

// TODO: rework into promises
class CertScanner extends EventEmitter {
  constructor(sslDirectory, options) {
    super();
    
    this.options = options;
    
    options = options || {};

    if(!sslDirectory) {
      throw new Error('sslDirectory as first argument is mandatory');
    }

    this.sslDirectory = sslDirectory;
    if (!sslDirectory.match(/\/$/)) {
      this.sslDirectory += '/';
    }

    this.readPart = async.memoize(function(file, maxRead, cb) {
      fs.open(file, 'r', function(err, fd) {
        if(err) return cb(err);
        var buf = new Buffer(maxRead);
        fs.read(fd, buf, 0, maxRead, null, function(err, bytesRead, buffer) {
          fs.close(fd);
          if(bytesRead < maxRead)
            return cb(err, buffer.slice(0, bytesRead).toString('utf8'));
          return cb(err, buffer.toString('utf8'));
        });
      });
    });

    var beginCertToken = '-----BEGIN CERTIFICATE-----';
    var endCertToken = '-----END CERTIFICATE-----';
    this.getCaCertsFromFile = function(certPath, cb) {
      // TODO: This can possibly be replaced with some regexp.
      this.readPart(certPath, 4*65536, function(err, certFileContent) {
        if(err) return cb(err);

        var possibleCerts = certFileContent.split(beginCertToken);
        var certs = [];
        var rawCerts = [];
        possibleCerts.forEach(function(cert) {
          var endTokenIndex = cert.indexOf(endCertToken);
          if (endTokenIndex === -1) {
            return null;
          }
          var rawCert = cert.substring(0, endTokenIndex);
          var parsedCert = beginCertToken + rawCert + endCertToken + '\n';
          try {
            certs.push(x509.parseCert(parsedCert));
            rawCerts.push(parsedCert);
          } catch(err) {

          }
        });
        cb(null, certs, rawCerts);
      });
    };
    
    this.getCaFor = async.memoize((certPath, cb) => {
      this.readPart(certPath, 65636, (err, rawCert) => {
        if(err) return cb(err);

        var parsedCert;
        try {
          parsedCert = x509.parseCert(rawCert);
        } catch(err) {
          return cb(err);
        }
        var caResults = [];
        var caRawResults = [];
        let processDirectory = (dirName, cb) => {
          fs.readdir(dirName, (err, files) => {

            if(err) return cb(err);

            async.filter(files, (certFile, cb) => {
              var certPath = path.join(dirName, certFile);

              fs.stat(certPath, (err, statData) => {
                if(statData.isDirectory()) {
                  return processDirectory(certPath, () => {
                    cb(null, false); // is a directory
                  });
                }

                this.getCaCertsFromFile(certPath, (err, certs, rawCerts) => {
                  if(err) return cb(null, false);

                  if (this.isDomainCert(certs)) {
                    return cb(null, false);
                  }
                  var matchingCa = certs.filter((cert, i) => {
                    var res = this.caMatches(cert, parsedCert);

                    if(res) {
                      caRawResults.push(rawCerts[i]);
                    }
                    return res;
                  });
                  return cb(null, matchingCa.length);
                });
              });
            }, function(err, ca) {
              caResults = ca.map(fileName => {
                return path.join(dirName, fileName);
              }).concat(caResults);

              cb(null);
            });
          });
        }
        processDirectory(this.sslDirectory, err => {
          if(err) return cb(err);

          function noArrayIfOne(arr) {
            return (arr.length === 1) ? arr[0] : arr;
          }

          if(caResults.length) {
            cb(null, noArrayIfOne(caResults), noArrayIfOne(caRawResults));
          }
          else {
            cb(null);
          }
        });
      });
    });
  }
  
  scan(cb) {
    var outputConfig = {};

    var keys = {};
    var certs = {};

    let options = this.options;

    let processDirectory = (dirName, cb) => {
      fs.readdir(dirName, (err, files) => {
        if(err) return cb(err);

        async.each(files, (certFile, cb) => {
          var certPath = path.join(dirName, certFile);

          fs.stat(certPath, (err, statData) => {
            if(err) return cb(err);

            if(statData.isDirectory())
              return processDirectory(certPath, err => {
                if(err && err.code === 'EACCES') {
                  this.emit('notice', path.join(dirName, certPath) + ': directory is not accessible');
                  return cb(null);
                }
                cb(err);
              });

            this.getCertOrPem(certPath, (err, cert, pem, rawCert) => {
              if(pem) {
                keys[pem.publicModulus] = options.read ? rawCert : certPath;
                return cb();
              }
              if(err)  {
                if(err.code === 'EACCES') {
                  this.emit('notice', path.join(dirName, certPath) + ': file is not readable');
                  return cb(null);
                }
                if(err.toString().match(/Certificate data larger than/))
                  return cb(null);
                if(err.toString().match(/Unable to parse/))
                  return cb(null);
                if(err.toString().match(/Certificate argument provided, but left blank/))
                  return cb(null);
                return cb(err);
              }
              if(!err && !cert && !pem) {
                return cb();
              }


              if(!options.acceptInvalidDates) {
                var notBeforeMoment = moment(new Date(cert.notBefore));
                var notAfterMoment = moment(new Date(cert.notAfter));
                var nowMoment = moment();
                if(notBeforeMoment.diff(nowMoment) < 0) { // valid
                  if(notAfterMoment.diff(nowMoment) > 0) { // valid
                    if(notAfterMoment.diff(nowMoment, 'd') < 90) {
                      var daysValid = notAfterMoment.diff(nowMoment, 'd').toString();
                      var hoursValid = notAfterMoment.diff(nowMoment, 'h') - daysValid*24;
                      this.emit('notice', path.join(dirName, certPath) + ': valid only for ' + daysValid + ' days ' + hoursValid + ' hours');
                    }
                  }
                  else { //expired
                    this.emit('notice', path.join(dirName, certPath) + ': expired ' + (-notAfterMoment.diff(nowMoment, 'd')).toString() + ' days ago');
                    return cb();
                  }
                }
                else { // not yet valid
                  this.emit('notice', path.join(dirName, certPath) + ': not yet valid for ' + (notBeforeMoment.diff(nowMoment, 'd')).toString() + ' days');
                  return cb();
                }
              }

              

              var altNames = cert.altNames;

              var keyForCert = options.read ? rawCert : certPath;
              certs[keyForCert] = cert.publicModulus;

              async.each(altNames, (domain, cb) => {
                outputConfig[domain] = {};
                outputConfig[domain].cert = options.read ? rawCert : certPath;

                this.getCaFor(certPath, (err, ca, caRaw) => {
                  if (ca) {
                    outputConfig[domain].ca = options.read ? caRaw : ca;
                  }
                  cb(err);
                });
              }, cb);

            });


          });
        }, function(err) {
          cb(err);
        });
      });
    }
    processDirectory(this.sslDirectory, function(err) {

      Object.keys(outputConfig).forEach(function(domain) {
        var key = keys[certs[outputConfig[domain].cert]];


        // flatten CA reuslts array by removing duplicates
        var ca = outputConfig[domain].ca;
        if(ca && ca instanceof Array) {
          ca = ca.filter(function(elem, pos) {
            return ca.indexOf(elem) == pos;
          });
          if(ca.length === 1)
            outputConfig[domain].ca = ca[0];
          else
            outputConfig[domain].ca = ca;
        }


        if(key) {
          outputConfig[domain].key = key;
        }
        else if(options.onlyWithKey) {
          delete outputConfig[domain];
        }
      });

      cb(err, outputConfig);
    });
  }
  
  getCertOrPem(certPath, cb) {
    this.readPart(certPath, 65536, function(err, rawCert) {
      if(err) return cb(err);
      try {
        var cert = x509.parseCert(rawCert);
        return cb(null, cert, null, rawCert);
        
      } catch(err) {
        try {
          var pem = x509.parseKey(rawCert);

          return cb(null, null, pem, rawCert);
        } catch(err2) {

        }
        cb(err);
      }
    });
  }

  isDomainCert(certs) {
    return certs.some(function(cert) {
      return cert.altNames.length > 0;
    });
  }

  caMatches(caCert, issuedCertificatedbyCA) {
    let subject = caCert.subject;
    let expectedIssuer = issuedCertificatedbyCA.issuer;
    let status =
     expectedIssuer.countryName === subject.countryName
        && expectedIssuer.organizationName === subject.organizationName
        && expectedIssuer.organizationalUnitName === subject.organizationalUnitName
        && expectedIssuer.commonName === subject.commonName
       && caCert.publicModulo === issuedCertificatedbyCA.publicModulo
       && caCert.publicExponent === issuedCertificatedbyCA.publicExponent
        ;

    return status;
  }
}

module.exports = CertScanner;