// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

if (!process.versions.openssl) {
  console.error("Skipping because node compiled without OpenSSL.");
  process.exit(0);
}

// This is a rather complex test which sets up various TLS servers with node
// and connects to them using the 'openssl s_client' command line utility
// with various keys. Depending on the certificate authority and other
// parameters given to the server, the various clients are
// - rejected,
// - accepted and "unauthorized", or
// - accepted and "authorized".

var testCases =
  [ { title: "Do not request certs. Everyone is unauthorized.",
      requestCert: false,
      rejectUnauthorized: false,
      CAs: ['ca1-cert'],
      clients:
        [ { name: 'agent1', shouldReject: false, shouldAuth: false },
          { name: 'agent2', shouldReject: false, shouldAuth: false },
          { name: 'agent3', shouldReject: false, shouldAuth: false },
          { name: 'nocert', shouldReject: false, shouldAuth: false }
        ]
    },

    { title: "Allow both authed and unauthed connections with CA1",
      requestCert: true,
      rejectUnauthorized: false,
      CAs: ['ca1-cert'],
      clients:
        [ { name: 'agent1', shouldReject: false, shouldAuth: true },
          { name: 'agent2', shouldReject: false, shouldAuth: false },
          { name: 'agent3', shouldReject: false, shouldAuth: false },
          { name: 'nocert', shouldReject: false, shouldAuth: false }
        ]
    },

    { title: "Allow only authed connections with CA1",
      requestCert: true,
      rejectUnauthorized: true,
      CAs: ['ca1-cert'],
      clients:
        [ { name: 'agent1', shouldReject: false, shouldAuth: true },
          { name: 'agent2', shouldReject: true },
          { name: 'agent3', shouldReject: true },
          { name: 'nocert', shouldReject: true }
        ]
    },

    { title: "Allow only authed connections with CA1 and CA2",
      requestCert: true,
      rejectUnauthorized: true,
      CAs: ['ca1-cert', 'ca2-cert'],
      clients:
        [ { name: 'agent1', shouldReject: false, shouldAuth: true },
          { name: 'agent2', shouldReject: true },
          { name: 'agent3', shouldReject: false, shouldAuth: true },
          { name: 'nocert', shouldReject: true }
        ]
    },


    { title: "Allow only certs signed by CA2 but not in the CRL",
      requestCert: true,
      rejectUnauthorized: true,
      CAs: ['ca2-cert'],
      crl: 'ca2-crl',
      clients:
        [ { name: 'agent1', shouldReject: true, shouldAuth: false },
          { name: 'agent2', shouldReject: true, shouldAuth: false  },
          { name: 'agent3', shouldReject: false, shouldAuth: true },
          // Agent4 has a cert in the CRL.
          { name: 'agent4', shouldReject: true, shouldAuth: false },
          { name: 'nocert', shouldReject: true }
        ]
    },
  ];


var common = require('../common');
var assert = require('assert');
var fs = require('fs');
var tls = require('tls');
var spawn = require('child_process').spawn;


function filenamePEM(n) {
  return require('path').join(common.fixturesDir, 'keys', n + ".pem");
}


function loadPEM(n) {
  return fs.readFileSync(filenamePEM(n));
}


var serverKey = loadPEM('agent2-key');
var serverCert = loadPEM('agent2-cert');


function runClient (options, cb) {

  // Client can connect in three ways:
  // - Self-signed cert
  // - Certificate, but not signed by CA.
  // - Certificate signed by CA.

  var args = ['s_client', '-connect', '127.0.0.1:' + common.PORT];


  console.log("  connecting with", options.name);

  switch (options.name) {
    case 'agent1':
      // Signed by CA1
      args.push('-key');
      args.push(filenamePEM('agent1-key'));
      args.push('-cert');
      args.push(filenamePEM('agent1-cert'));
      break;

    case 'agent2':
      // Self-signed
      // This is also the key-cert pair that the server will use.
      args.push('-key');
      args.push(filenamePEM('agent2-key'));
      args.push('-cert');
      args.push(filenamePEM('agent2-cert'));
      break;

    case 'agent3':
      // Signed by CA2
      args.push('-key');
      args.push(filenamePEM('agent3-key'));
      args.push('-cert');
      args.push(filenamePEM('agent3-cert'));
      break;

    case 'agent4':
      // Signed by CA2 (rejected by ca2-crl)
      args.push('-key');
      args.push(filenamePEM('agent4-key'));
      args.push('-cert');
      args.push(filenamePEM('agent4-cert'));
      break;

    case 'nocert':
      // Do not send certificate
      break;

    default:
      throw new Error("Unknown agent name");
  }

  // To test use: openssl s_client -connect localhost:8000
  var client = spawn('openssl', args);

  var out = '';

  var rejected = true;
  var authed = false;

  client.stdout.setEncoding('utf8');
  client.stdout.on('data', function(d) {
    out += d;

    if (/_unauthed/g.test(out)) {
      console.error("  * unauthed");
      client.stdin.end('goodbye\n');
      authed = false;
      rejected = false;
    }

    if (/_authed/g.test(out)) {
      console.error("  * authed");
      client.stdin.end('goodbye\n');
      authed = true;
      rejected = false;
    }
  });

  //client.stdout.pipe(process.stdout);

  client.on('exit', function(code) {
    //assert.equal(0, code, options.name +
    //      ": s_client exited with error code " + code);
    if (options.shouldReject) {
      assert.equal(true, rejected, options.name +
          " NOT rejected, but should have been");
    } else {
      assert.equal(false, rejected, options.name +
          " rejected, but should NOT have been");
      assert.equal(options.shouldAuth, authed);
    }

    cb();
  });
}


// Run the tests
var successfulTests = 0;
function runTest (testIndex) {
  var tcase = testCases[testIndex];
  if (!tcase) return;

  console.error("Running '%s'", tcase.title);

  var cas = tcase.CAs.map(loadPEM);

  var crl = tcase.crl ? loadPEM(tcase.crl) : null;

  var serverOptions = {
    key: serverKey,
    cert: serverCert,
    ca: cas,
    crl: crl,
    requestCert: tcase.requestCert,
    rejectUnauthorized: tcase.rejectUnauthorized
  };

  var connections = 0;

  var server = tls.Server(serverOptions, function (c) {
    connections++;
    if (c.authorized) {
      console.error('- authed connection: ' +
                    c.getPeerCertificate().subject.CN);
      c.write('\n_authed\n');
    } else {
      console.error('- unauthed connection: %s', c.authorizationError);
      c.write('\n_unauthed\n');
    }
  });

  function runNextClient(clientIndex) {
    var options = tcase.clients[clientIndex];
    if (options) {
      runClient(options, function () {
        runNextClient(clientIndex + 1);
      });
    } else {
      server.close();
      successfulTests++;
      runTest(testIndex + 1);
    }
  }

  server.listen(common.PORT, function() {
    if (tcase.debug) {
      console.error("TLS server running on port " + common.PORT);
    } else {
      runNextClient(0);
    }
  });
}


runTest(0);


process.on('exit', function() {
  assert.equal(successfulTests, testCases.length);
});
