'use strict';

delete require.cache['./certGenerator'];

var path = require('path'),
    fs = require('fs'),
    color = require('colorful'),
    certGenerator = require('./certGenerator'),
    util = require('./util'),
    Errors = require('./errorConstants'),
    https = require('https'),
    AsyncTask = require('async-task-mgr'),
    winCertUtil = require('./winCertUtil'),
    cryptoX = require('crypto'),
    exec = require('child_process').exec;
    
var forge = require('node-forge');

var crtlist = [];

setInterval(() => {
  if(crtlist.length<80){
    getRsaKeys((pkey,pub)=>{crtlist.push({'pkey':pkey,'pub':pub})});
  }
}, 500);

var DOMAIN_TO_VERIFY_HTTPS = 'localtest.me';

function getPort() {
  return new Promise(function (resolve, reject) {
    var server = require('net').createServer();
    server.unref();
    server.on('error', reject);
    server.listen(0, function () {
      var port = server.address().port;
      server.close(function () {
        resolve(port);
      });
    });
  });
}

function CertManager(options) {
  options = options || {};
  var rootDirName = util.getDefaultRootDirName();
  var rootDirPath = options.rootDirPath || path.join(util.getUserHome(), '/' + rootDirName + '/');

  if (options.defaultCertAttrs) {
    certGenerator.setDefaultAttrs(options.defaultCertAttrs);
  }

  var certDir = rootDirPath,
      rootCAcrtFilePath = path.join(certDir, 'rootCA.crt'),
      rootCAkeyFilePath = path.join(certDir, 'rootCA.key'),
      createCertTaskMgr = new AsyncTask();
  var cacheRootCACrtFileContent = 0,
      cacheRootCAKeyFileContent = 0;
  var rootCAExists = false;

  if (!fs.existsSync(certDir)) {
    try {
      fs.mkdirSync(certDir, '0777');
    } catch (e) {
      console.log('===========');
      console.log('failed to create cert dir ,please create one by yourself - ' + certDir);
      console.log('===========');
    }
  }

  function getCertificate222(hostname, certCallback) {
    if (!_checkRootCA()) {
      console.log(color.yellow('please generate root CA before getting certificate for sub-domains'));
      certCallback && certCallback(Errors.ROOT_CA_NOT_EXISTS);
      return;
    }
    var keyFile = path.join(certDir, '__hostname.key'.replace(/__hostname/, hostname)),
        crtFile = path.join(certDir, '__hostname.crt'.replace(/__hostname/, hostname));

    if (!cacheRootCACrtFileContent || !cacheRootCAKeyFileContent) {
      cacheRootCACrtFileContent = fs.readFileSync(rootCAcrtFilePath, { encoding: 'utf8' });
      cacheRootCAKeyFileContent = fs.readFileSync(rootCAkeyFilePath, { encoding: 'utf8' });
    }

    createCertTaskMgr.addTask(hostname, function (callback) {
      if (!fs.existsSync(keyFile) || !fs.existsSync(crtFile)) {
        try {
          var result = certGenerator.generateCertsForHostname(hostname, {
            cert: cacheRootCACrtFileContent,
            key: cacheRootCAKeyFileContent
          });
          fs.writeFileSync(keyFile, result.privateKey);
          fs.writeFileSync(crtFile, result.certificate);
          callback(null, result.privateKey, result.certificate);
        } catch (e) {
          callback(e);
        }
      } else {
        callback(null, fs.readFileSync(keyFile), fs.readFileSync(crtFile));
      }
    }, function (err, keyContent, crtContent) {
      if (!err) {
        certCallback(null, keyContent, crtContent);
      } else {
        certCallback(err);
      }
    });
  }



  cacheRootCACrtFileContent = fs.readFileSync(rootCAcrtFilePath, { encoding: 'utf8' });
  cacheRootCAKeyFileContent = fs.readFileSync(rootCAkeyFilePath, { encoding: 'utf8' });
  var caCert = forge.pki.certificateFromPem(cacheRootCACrtFileContent);
  var caKey = forge.pki.privateKeyFromPem(cacheRootCAKeyFileContent);
  /*
  console.log(cacheRootCAKeyFileContent);
  
  var wcaKey;
  window.crypto.subtle.importKey(
    "pkcs8",
    pemToArrayBuffer(fs.readFileSync(rootCAkeyFilePath+'.pkcs8.pem', { encoding: 'utf8' })+''),
    {
      name: "RSASSA-PKCS1-v1_5",
      modulusLength: 2048, //can be 1024, 2048, or 4096
      publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
      hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
  },
  true, //whether the key is extractable (i.e. can be used in exportKey)
  ['sign'] //must be ["encrypt", "decrypt"] or ["wrapKey", "unwrapKey"]
  ).then(function(key) {
    wcaKey=key;
  });
  */
  function getCertificate(hostname, certCallback) {

    const keyFile = path.join(certDir, '__hostname.key'.replace(/__hostname/, hostname)),
      crtFile = path.join(certDir, '__hostname.crt'.replace(/__hostname/, hostname));
    if (fs.existsSync(keyFile) && fs.existsSync(crtFile)) {
      certCallback(null, fs.readFileSync(keyFile), fs.readFileSync(crtFile));
      return;
    }

    var lsls =(pkey,pub)=>{
      var md = forge.md.md5.create();
      md.update(hostname);

      var serialNumber = md.digest().toHex();
      var keys ={};
      keys.publicKey = forge.pki.publicKeyFromPem(pub);
      keys.privateKey = forge.pki.privateKeyFromPem(pkey);
      var cert = forge.pki.createCertificate();
      cert.publicKey = keys.publicKey;
      cert.serialNumber = serialNumber;
      cert.validity.notBefore = new Date();
      cert.validity.notBefore.setFullYear(cert.validity.notBefore.getFullYear() - 10); // 10 years
      cert.validity.notAfter = new Date();
      cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 10); // 10 years
      cert.setIssuer(caCert.subject.attributes);
      cert.setSubject([{ name: 'countryName', value: 'CN' }, { name: 'organizationName', value: 'LingMax HTTPS Proxy' }, { shortName: 'ST', value: 'SH' }, { shortName: 'OU', value: 'LingMax SSL Proxy from AnyProxy' },{name: 'commonName', value: hostname}]);
      cert.setExtensions([{ name: 'basicConstraints', cA: false },{name: 'subjectAltName',altNames: [{ type:/^\d+?\.\d+?\.\d+?\.\d+?$/.test(hostname)?7:2, value: hostname }]}]);
      
      //cert.sign(caKey, forge.md.sha256.create());
      
      
      cert.signatureOid = cert.siginfo.algorithmOid = "1.2.840.113549.1.1.11";
      cert.tbsCertificate = forge.pki.getTBSCertificate(cert);
      var bytes = forge.asn1.toDer(cert.tbsCertificate);
      //var tt ;tt =  new Date().getTime();tt =  new Date().getTime();

      //node自带签名 8ms
      var ss = cryptoX.createSign('RSA-SHA256');
      ss.update(bytes.data);
      cert.signature = ss.sign(cacheRootCAKeyFileContent,'binary');
      //console.log('node签名',new Date().getTime()-tt);
      
      
        
        /*
        //浏览器签名 200ms
        tt =  new Date().getTime();
       window.crypto.subtle.sign("RSASSA-PKCS1-v1_5",wcaKey,Buffer.from(bytes.data,'binary'))
      .then(function(T2) {
        //console.log(11111,arguments);
        cert.signature = Buffer.from(T2).toString('binary');
        console.log('浏览器签名',new Date().getTime()-tt);
        var crtStr = forge.pki.certificateToPem(cert);
        fs.writeFileSync(keyFile, pkey);
        fs.writeFileSync(crtFile, crtStr);
        certCallback(null, pkey, crtStr);
        
      });
      return;
      */

      /*
      //原生签名 手写代码 98ms
      tt =  new Date().getTime();
      cert.md = forge.md.sha256.create();
      cert.md.update(bytes.getBytes());
      var encoded = caKey.emsaPkcs1v15encode(cert.md,caKey.n.bitLength());
      cert.signature = forge.pki.rsa.encrypt(encoded, caKey, 1);
      console.log('原生签名',new Date().getTime()-tt);
      */
      
      
      var crtStr = forge.pki.certificateToPem(cert);
      fs.writeFileSync(keyFile, pkey);
      fs.writeFileSync(crtFile, crtStr);
      certCallback(null, pkey, crtStr);
    };
    var lllss = crtlist.pop(); //动态调度计划
    if(lllss){
      lsls(lllss.pkey,lllss.pub);
    }else{
      getRsaKeys(lsls);
    }
    
  }


  function clearCerts(cb) {
    util.deleteFolderContentsRecursive(certDir);
    cb && cb();
  }

  function isRootCAFileExists() {
    return fs.existsSync(rootCAcrtFilePath) && fs.existsSync(rootCAkeyFilePath);
  }

  function generateRootCA(config, certCallback) {
    if (!config || !config.commonName) {
      console.error(color.red('The "config.commonName" for rootCA is required, please specify.'));
      certCallback(Errors.ROOT_CA_COMMON_NAME_UNSPECIFIED);
      return;
    }

    if (isRootCAFileExists()) {
      if (config.overwrite) {
        startGenerating(config.commonName, certCallback);
      } else {
        console.error(color.red('The rootCA exists already, if you want to overwrite it, please specify the "config.overwrite=true"'));
        certCallback(Errors.ROOT_CA_EXISTED);
      }
    } else {
      startGenerating(config.commonName, certCallback);
    }

    function startGenerating(commonName, cb) {
      // clear old certs
      clearCerts(function () {
        console.log(color.green('temp certs cleared'));
        try {
          var result = certGenerator.generateRootCA(commonName);
          fs.writeFileSync(rootCAkeyFilePath, result.privateKey);
          fs.writeFileSync(rootCAcrtFilePath, result.certificate);

          console.log(color.green('rootCA generated'));
          console.log(color.green(color.bold('PLEASE TRUST the rootCA.crt in ' + certDir)));

          cb && cb(null, rootCAkeyFilePath, rootCAcrtFilePath);
        } catch (e) {
          console.log(color.red(e));
          console.log(color.red(e.stack));
          console.log(color.red('fail to generate root CA'));
          cb && cb(e);
        }
      });
    }
  }

  function getRootCAFilePath() {
    return isRootCAFileExists() ? rootCAcrtFilePath : '';
  }

  function getRootDirPath() {
    return rootDirPath;
  }

  function _checkRootCA() {
    if (rootCAExists) {
      return true;
    }

    if (!isRootCAFileExists()) {
      console.log(color.red('can not find rootCA.crt or rootCA.key'));
      console.log(color.red('you may generate one'));
      return false;
    } else {
      rootCAExists = true;
      return true;
    }
  }

  function ifRootCATrusted(callback) {
    if (!isRootCAFileExists()) {
      callback && callback(new Error('ROOTCA_NOT_EXIST'));
    } else if (/^win/.test(process.platform)) {
      winCertUtil.ifWinRootCATrusted().then(function (ifTrusted) {
        callback && callback(null, ifTrusted);
      }).catch(function (e) {
        callback && callback(null, false);
      });
    } else {
      var HTTPS_RESPONSE = 'HTTPS Server is ON';
      // localtest.me --> 127.0.0.1
      getCertificate(DOMAIN_TO_VERIFY_HTTPS, function (e, key, cert) {
        getPort().then(function (port) {
          if (e) {
            callback && callback(e);
            return;
          }
          var server = https.createServer({
            ca: fs.readFileSync(rootCAcrtFilePath),
            key: key,
            cert: cert
          }, function (req, res) {
            res.end(HTTPS_RESPONSE);
          }).listen(port);

          // do not use node.http to test the cert. Ref: https://github.com/nodejs/node/issues/4175
          var testCmd = 'curl https://' + DOMAIN_TO_VERIFY_HTTPS + ':' + port;
          exec(testCmd, { timeout: 1000 }, function (error, stdout, stderr) {
            server.close();
            if (error) {
              callback && callback(null, false);
            }
            if (stdout && stdout.indexOf(HTTPS_RESPONSE) >= 0) {
              callback && callback(null, true);
            } else {
              callback && callback(null, false);
            }
          });
        }).catch(callback);
      });
    }
  }

  return {
    getRootCAFilePath: getRootCAFilePath,
    generateRootCA: generateRootCA,
    getCertificate: getCertificate,
    clearCerts: clearCerts,
    isRootCAFileExists: isRootCAFileExists,
    ifRootCATrusted: ifRootCATrusted,
    getRootDirPath: getRootDirPath
  };
}

  //获取密钥对
  function getRsaKeys(func){
    window.crypto.subtle.generateKey(
        {
            name: "RSA-OAEP",
            modulusLength: 2048, //can be 1024, 2048, or 4096
            publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
            hash: {name: "SHA-512"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
        },
        true, //whether the key is extractable (i.e. can be used in exportKey)
        ["encrypt", "decrypt"] //must be ["encrypt", "decrypt"] or ["wrapKey", "unwrapKey"]
    ).then(function(key){
        window.crypto.subtle.exportKey(
            "pkcs8", 
            key.privateKey 
        ).then(function(keydata1){
            window.crypto.subtle.exportKey(
                "spki",
                key.publicKey 
            ).then(function(keydata2){
                try {
                  var privateKey = RSA2text(keydata1,1);
                var publicKey = RSA2text(keydata2);
                  func(privateKey,publicKey);
                } catch (error) {
                  throw error;
                }
                
            }).catch(function(err){
                console.error(err);
            });
        })
        .catch(function(err){
            console.error(err);
        });
    })
    .catch(function(err){
        console.error(err);
    });
  
    function RSA2text(buffer,isPrivate=0) {
        var binary = '';
        var bytes = new Uint8Array(buffer);
        var len = bytes.byteLength;
        for (var i = 0; i < len; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        var base64 = window.btoa(binary);
        var text = "-----BEGIN "+(isPrivate?"PRIVATE":"PUBLIC")+" KEY-----\n";
        text += base64.replace(/[^\x00-\xff]/g,"$&\x01").replace(/.{64}\x01?/g,"$&\n");
        text += "\n-----END "+(isPrivate?"PRIVATE":"PUBLIC")+" KEY-----";
        return text;
    }
  }

  function pemToArrayBuffer(pem) {
    var b64Lines = pem
    .replace(/-----[\sA-Z]{3,64}-----/g, '')
    .replace(/\r/g, '').replace(/\n/g, '')
    ;
    var byteString = window.atob(b64Lines);
    var byteArray = new Uint8Array(byteString.length);
    for (var i = 0; i < byteString.length; i++) {
      byteArray[i] = byteString.charCodeAt(i);
    }
    return byteArray;
  }

module.exports = CertManager;