# openssl.js

    var helper = require('./helper.js')
    var cpspawn = require('child_process').spawn
    var pathlib = require('path')
    var fs = require('fs')
    var osTmpdir = require('os-tmpdir')
    var crypto = require('crypto')
    var which = require('which')
    var settings = {}
    var tempDir = process.env.PEMJS_TMPDIR || osTmpdir()

    /**
     * pem openssl module
     *
     * @module openssl
     */

    /**
     * configue this openssl module
     *
     * @static
     * @param {String} option name e.g. pathOpenSSL, openSslVersion; TODO rethink nomenclature
     * @param {*} value value
     */
    function set (option, value) {
      settings[option] = value
    }

    /**
     * get configuration setting value
     *
     * @static
     * @param {String} option name
     */
    function get (option) {
      return settings[option] || null
    }

    /**
     * Spawn an openssl command
     *
     * @static
     * @param {Array} params Array of openssl command line parameters
     * @param {String} searchStr String to use to find data
     * @param {Array} [tmpfiles] list of temporary files
     * @param {Function} callback Called with (error, stdout-substring)
     */
    function exec (params, searchStr, tmpfiles, callback) {
      if (!callback && typeof tmpfiles === 'function') {
        callback = tmpfiles
        tmpfiles = false
      }

      spawnWrapper(params, tmpfiles, function (err, code, stdout, stderr) {
        var start, end

        if (err) {
          return callback(err)
        }

        if ((start = stdout.match(new RegExp('\\-+BEGIN ' + searchStr + '\\-+$', 'm')))) {
          start = start.index
        } else {
          start = -1
        }

        // To get the full EC key with parameters and private key
        if (searchStr === 'EC PARAMETERS') {
          searchStr = 'EC PRIVATE KEY'
        }

        if ((end = stdout.match(new RegExp('^\\-+END ' + searchStr + '\\-+', 'm')))) {
          end = end.index + end[0].length
        } else {
          end = -1
        }

        if (start >= 0 && end >= 0) {
          return callback(null, stdout.substring(start, end))
        } else {
          return callback(new Error(searchStr + ' not found from openssl output:\n---stdout---\n' + stdout + '\n---stderr---\n' + stderr + '\ncode: ' + code))
        }
      })
    }

    /**
     *  Spawn an openssl command and get binary output
     *
     * @static
     * @param {Array} params Array of openssl command line parameters
     * @param {Array} [tmpfiles] list of temporary files
     * @param {Function} callback Called with (error, stdout)
    */
    function execBinary (params, tmpfiles, callback) {
      if (!callback && typeof tmpfiles === 'function') {
        callback = tmpfiles
        tmpfiles = false
      }
      spawnWrapper(params, tmpfiles, true, function (err, code, stdout, stderr) {
        if (err) {
          return callback(err)
        }
        return callback(null, stdout)
      })
    }

    /**
     * Generically spawn openSSL, without processing the result
     *
     * @static
     * @param {Array}        params   The parameters to pass to openssl
     * @param {Boolean}      binary   Output of openssl is binary or text
     * @param {Function}     callback Called with (error, exitCode, stdout, stderr)
     */
    function spawn (params, binary, callback) {
      var pathBin = get('pathOpenSSL') || process.env.OPENSSL_BIN || 'openssl'

      testOpenSSLPath(pathBin, function (err) {
        if (err) {
          return callback(err)
        }
        var openssl = cpspawn(pathBin, params)
        var stderr = ''

        var stdout = (binary ? Buffer.alloc(0) : '')
        openssl.stdout.on('data', function (data) {
          if (!binary) {
            stdout += data.toString('binary')
          } else {
            stdout = Buffer.concat([stdout, data])
          }
        })

        openssl.stderr.on('data', function (data) {
          stderr += data.toString('binary')
        })
        // We need both the return code and access to all of stdout.  Stdout isn't
        // *really* available until the close event fires; the timing nuance was
        // making this fail periodically.
        var needed = 2 // wait for both exit and close.
        var code = -1
        var finished = false
        var done = function (err) {
          if (finished) {
            return
          }

          if (err) {
            finished = true
            return callback(err)
          }

          if (--needed < 1) {
            finished = true
            if (code) {
              if (code === 2 && (stderr === '' || /depth lookup: unable to/.test(stderr))) {
                return callback(null, code, stdout, stderr)
              }
              return callback(new Error('Invalid openssl exit code: ' + code + '\n% openssl ' + params.join(' ') + '\n' + stderr), code)
            } else {
              return callback(null, code, stdout, stderr)
            }
          }
        }

        openssl.on('error', done)

        openssl.on('exit', function (ret) {
          code = ret
          done()
        })

        openssl.on('close', function () {
          stdout = (binary ? stdout : Buffer.from(stdout, 'binary').toString('utf-8'))
          stderr = Buffer.from(stderr, 'binary').toString('utf-8')
          done()
        })
      })
    }

    /**
     * Wrapper for spawn method
     *
     * @static
     * @param {Array} params The parameters to pass to openssl
     * @param {Array} [tmpfiles] list of temporary files
     * @param {Boolean} [binary] Output of openssl is binary or text
     * @param {Function} callback Called with (error, exitCode, stdout, stderr)
     */
    function spawnWrapper (params, tmpfiles, binary, callback) {
      if (!callback && typeof binary === 'function') {
        callback = binary
        binary = false
      }

      var files = []
      var delTempPWFiles = []

      if (tmpfiles) {
        tmpfiles = [].concat(tmpfiles)
        var fpath, i
        for (i = 0; i < params.length; i++) {
          if (params[i] === '--TMPFILE--') {
            fpath = pathlib.join(tempDir, crypto.randomBytes(20).toString('hex'))
            files.push({
              path: fpath,
              contents: tmpfiles.shift()
            })
            params[i] = fpath
            delTempPWFiles.push(fpath)
          }
        }
      }

      var file
      for (i = 0; i < files.length; i++) {
        file = files[i]
        fs.writeFileSync(file.path, file.contents)
      }

      spawn(params, binary, function (err, code, stdout, stderr) {
        helper.deleteTempFiles(delTempPWFiles, function (fsErr) {
          callback(err || fsErr, code, stdout, stderr)
        })
      })
    }

    /**
     * Validates the pathBin for the openssl command
     *
     * @private
     * @param {String} pathBin The path to OpenSSL Bin
     * @param {Function} callback Callback function with an error object
     */
    function testOpenSSLPath (pathBin, callback) {
      which(pathBin, function (error) {
        if (error) {
          return callback(new Error('Could not find openssl on your system on this path: ' + pathBin))
        }
        callback()
      })
    }

    /* Once PEM is imported, the openSslVersion is set with this function. */
    spawn(['version'], false, function (err, code, stdout, stderr) {
      var text = String(stdout) + '\n' + String(stderr) + '\n' + String(err)
      var tmp = text.match(/^LibreSSL/i)
      set('openSslVersion', (tmp && tmp[0] ? 'LibreSSL' : 'openssl').toUpperCase())
    })

    module.exports = {
      exec: exec,
      execBinary: execBinary,
      spawn: spawn,
      spawnWrapper: spawnWrapper,
      set: set,
      get: get
    }