/*
 * Copyright (c) 2004, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.test.ssl;

import org.postgresql.test.TestUtil;

import junit.framework.TestCase;
import junit.framework.TestSuite;

import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

public class SslTest extends TestCase {

  /**
   * Tries to connect to the database.
   *
   * @param connstr Connection string for the database
   * @param expected Expected values. the first element is a String holding the expected message of
   *        PSQLException or null, if no exception is expected, the second indicates weather ssl is
   *        to be used (Boolean)
   */
  protected void driver(String connstr, Object[] expected) throws SQLException {
    Connection conn = null;
    String exmsg = (String) expected[0];
    try {
      conn = DriverManager.getConnection(connstr, TestUtil.getUser(), TestUtil.getPassword());
      if (exmsg != null) {
        fail("Exception did not occur: " + exmsg);
      }
      //
      ResultSet rs = conn.createStatement().executeQuery("select ssl_is_used()");
      assertTrue(rs.next());
      assertEquals("ssl_is_used: ", ((Boolean) expected[1]).booleanValue(), rs.getBoolean(1));
      conn.close();
    } catch (SQLException ex) {
      if (conn != null) {
        conn.close();
      }
      if (exmsg == null) { // no exception is excepted
        fail("Exception thrown: " + ex.getMessage());
      } else {
        assertTrue("expected: " + exmsg + " actual: " + ex.getMessage(),
            ex.getMessage().matches(exmsg));
        return;
      }
    }
  }

  protected String certdir;
  protected String connstr;
  protected String sslmode;
  protected int protocol;
  protected boolean goodclient;
  protected boolean goodserver;
  protected String prefix;
  protected Object[] expected;

  private String makeConnStr(String sslmode, boolean goodclient, boolean goodserver, int protocol) {
    return connstr + "&protocolVersion=" + protocol
        + "&sslmode=" + sslmode
        + "&sslcert=" + certdir + "/" + prefix + (goodclient ? "goodclient.crt" : "badclient.crt")
        + "&sslkey=" + certdir + "/" + prefix + (goodclient ? "goodclient.pk8" : "badclient.pk8")
        + "&sslrootcert=" + certdir + "/" + prefix + (goodserver ? "goodroot.crt" : "badroot.crt")
        + "&loglevel=" + TestUtil.getLogLevel();
  }

  public SslTest(String name, String certdir, String connstr, String sslmode, int protocol,
      boolean goodclient, boolean goodserver, String prefix, Object[] expected) {
    super(name);
    this.certdir = certdir;
    this.connstr = connstr;
    this.sslmode = sslmode;
    this.protocol = protocol;
    this.goodclient = goodclient;
    this.goodserver = goodserver;
    this.prefix = prefix;
    this.expected = expected;
  }

  static TestSuite getSuite(Properties prop, String param) {
    File certDirFile = TestUtil.getFile(prop.getProperty("certdir"));
    String certdir = certDirFile.getAbsolutePath();
    String sconnstr = prop.getProperty(param);
    String sprefix = prop.getProperty(param + "prefix");
    String[] sslModes = {"disable", "allow", "prefer", "require", "verify-ca", "verify-full"};

    TestSuite suite = new TestSuite();
    Map<String, Object[]> expected = expectedmap.get(param);
    if (expected == null) {
      expected = defaultexpected;
    }
    for (String sslMode : sslModes) {
      suite.addTest(new SslTest(param + "-" + sslMode + "GG2", certdir, sconnstr, sslMode,
          2, true, true, sprefix, expected.get(sslMode + "GG")));
      suite.addTest(new SslTest(param + "-" + sslMode + "GG3", certdir, sconnstr, sslMode,
          3, true, true, sprefix, expected.get(sslMode + "GG")));
      suite.addTest(new SslTest(param + "-" + sslMode + "GB2", certdir, sconnstr, sslMode,
          2, true, false, sprefix, expected.get(sslMode + "GB")));
      suite.addTest(new SslTest(param + "-" + sslMode + "GB3", certdir, sconnstr, sslMode,
          3, true, false, sprefix, expected.get(sslMode + "GB")));
      suite.addTest(new SslTest(param + "-" + sslMode + "BG2", certdir, sconnstr, sslMode,
          2, false, true, sprefix, expected.get(sslMode + "BG")));
      suite.addTest(new SslTest(param + "-" + sslMode + "BG3", certdir, sconnstr, sslMode,
          3, false, true, sprefix, expected.get(sslMode + "BG")));
    }
    return suite;
  }

  protected void runTest() throws Throwable {
    driver(makeConnStr(sslmode, goodclient, goodserver, protocol), expected);
  }

  static Map<String, Map<String, Object[]>> expectedmap;
  static TreeMap<String, Object[]> defaultexpected;

  // For some strange reason, the v2 driver begins these error messages by "Connection rejected: "
  // but the v3 does not.
  // Also, for v2 there are two spaces after FATAL:, and the message ends with "\n.".
  static String PG_HBA_ON =
      "(Connection rejected: )?FATAL:  ?no pg_hba.conf entry for host .*, user .*, database .*, SSL on(?s-d:.*)";
  static String PG_HBA_OFF =
      "(Connection rejected: )?FATAL:  ?no pg_hba.conf entry for host .*, user .*, database .*, SSL off(?s-d:.*)";
  static String FAILED = "The connection attempt failed.";
  static String BROKEN =
      "SSL error: (Broken pipe|Received fatal alert: unknown_ca|Connection reset)";
  static String SSLMODE = "Invalid sslmode value: (allow|prefer)";
  // static String UNKNOWN = "SSL error: Broken pipe";
  // static String UNKNOWN = "SSL error: Received fatal alert: unknown_ca";
  static String ANY = ".*";
  static String VALIDATOR =
      "SSL error: sun.security.validator.ValidatorException: PKIX path (building|validation) failed:.*";
  static String HOSTNAME = "The hostname .* could not be verified.";

  static {
    defaultexpected = new TreeMap<String, Object[]>();
    defaultexpected.put("disableGG", new Object[]{null, Boolean.FALSE});
    defaultexpected.put("disableGB", new Object[]{null, Boolean.FALSE});
    defaultexpected.put("disableBG", new Object[]{null, Boolean.FALSE});
    defaultexpected.put("allowGG", new Object[]{SSLMODE, Boolean.TRUE});
    defaultexpected.put("allowGB", new Object[]{SSLMODE, Boolean.TRUE});
    defaultexpected.put("allowBG", new Object[]{SSLMODE, Boolean.TRUE});
    defaultexpected.put("preferGG", new Object[]{SSLMODE, Boolean.TRUE});
    defaultexpected.put("preferGB", new Object[]{SSLMODE, Boolean.TRUE});
    defaultexpected.put("preferBG", new Object[]{SSLMODE, Boolean.TRUE});
    defaultexpected.put("requireGG", new Object[]{null, Boolean.TRUE});
    defaultexpected.put("requireGB", new Object[]{null, Boolean.TRUE});
    defaultexpected.put("requireBG", new Object[]{null, Boolean.TRUE});
    defaultexpected.put("verify-caGG", new Object[]{null, Boolean.TRUE});
    defaultexpected.put("verify-caGB", new Object[]{ANY, Boolean.TRUE});
    defaultexpected.put("verify-caBG", new Object[]{null, Boolean.TRUE});
    defaultexpected.put("verify-fullGG", new Object[]{null, Boolean.TRUE});
    defaultexpected.put("verify-fullGB", new Object[]{ANY, Boolean.TRUE});
    defaultexpected.put("verify-fullBG", new Object[]{null, Boolean.TRUE});

    expectedmap = new TreeMap<String, Map<String, Object[]>>();
    TreeMap<String, Object[]> work;

    work = (TreeMap) defaultexpected.clone();
    work.put("disableGG", new Object[]{null, Boolean.FALSE});
    work.put("disableGB", new Object[]{null, Boolean.FALSE});
    work.put("disableBG", new Object[]{null, Boolean.FALSE});
    work.put("allowGG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("allowGB", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("allowBG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("preferGG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("preferGB", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("preferBG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("requireGG", new Object[]{ANY, Boolean.TRUE});
    work.put("requireGB", new Object[]{ANY, Boolean.TRUE});
    work.put("requireBG", new Object[]{ANY, Boolean.TRUE});
    work.put("verify-caGG", new Object[]{ANY, Boolean.TRUE});
    work.put("verify-caGB", new Object[]{ANY, Boolean.TRUE});
    work.put("verify-caBG", new Object[]{ANY, Boolean.TRUE});
    work.put("verify-fullGG", new Object[]{ANY, Boolean.TRUE});
    work.put("verify-fullGB", new Object[]{ANY, Boolean.TRUE});
    work.put("verify-fullBG", new Object[]{ANY, Boolean.TRUE});
    expectedmap.put("ssloff8", work);
    work = (TreeMap) work.clone();
    expectedmap.put("ssloff9", work);

    work = (TreeMap) defaultexpected.clone();
    work.put("disableGG", new Object[]{null, Boolean.FALSE});
    work.put("disableGB", new Object[]{null, Boolean.FALSE});
    work.put("disableBG", new Object[]{null, Boolean.FALSE});
    work.put("allowGG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("allowGB", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("allowBG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("preferGG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("preferGB", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("preferBG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("requireGG", new Object[]{PG_HBA_ON, Boolean.TRUE});
    work.put("requireGB", new Object[]{PG_HBA_ON, Boolean.TRUE});
    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-caGG", new Object[]{PG_HBA_ON, Boolean.TRUE});
    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-fullGG", new Object[]{PG_HBA_ON, Boolean.TRUE});
    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
    expectedmap.put("sslhostnossl8", work);
    work = (TreeMap) work.clone();
    expectedmap.put("sslhostnossl9", work);

    work = (TreeMap) defaultexpected.clone();
    work.put("disableGG", new Object[]{null, Boolean.FALSE});
    work.put("disableGB", new Object[]{null, Boolean.FALSE});
    work.put("disableBG", new Object[]{null, Boolean.FALSE});
    work.put("allowGG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("allowGB", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("allowBG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("preferGG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferGB", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferBG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("requireGG", new Object[]{null, Boolean.TRUE});
    work.put("requireGB", new Object[]{null, Boolean.TRUE});
    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-caGG", new Object[]{null, Boolean.TRUE});
    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-fullGG", new Object[]{null, Boolean.TRUE});
    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
    expectedmap.put("sslhostgh8", work);
    work = (TreeMap) work.clone();
    expectedmap.put("sslhostgh9", work);

    work = (TreeMap) work.clone();
    work.put("disableGG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("disableGB", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("disableBG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("allowGG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("allowGB", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("allowBG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferBG", new Object[]{SSLMODE, Boolean.FALSE});
    expectedmap.put("sslhostsslgh8", work);
    work = (TreeMap) work.clone();
    expectedmap.put("sslhostsslgh9", work);

    work = (TreeMap) defaultexpected.clone();
    work.put("disableGG", new Object[]{null, Boolean.FALSE});
    work.put("disableGB", new Object[]{null, Boolean.FALSE});
    work.put("disableBG", new Object[]{null, Boolean.FALSE});
    work.put("allowGG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("allowGB", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("allowBG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("preferGG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferGB", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferBG", new Object[]{SSLMODE, Boolean.FALSE});
    work.put("requireGG", new Object[]{null, Boolean.TRUE});
    work.put("requireGB", new Object[]{null, Boolean.TRUE});
    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-caGG", new Object[]{null, Boolean.TRUE});
    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-fullGG", new Object[]{HOSTNAME, Boolean.TRUE});
    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
    expectedmap.put("sslhostbh8", work);
    work = (TreeMap) work.clone();
    expectedmap.put("sslhostbh9", work);

    work = (TreeMap) work.clone();
    work.put("disableGG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("disableGB", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("disableBG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("allowGG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("allowGB", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("allowBG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferBG", new Object[]{SSLMODE, Boolean.FALSE});
    expectedmap.put("sslhostsslbh8", work);
    work = (TreeMap) work.clone();
    expectedmap.put("sslhostsslbh9", work);

    work = (TreeMap) defaultexpected.clone();
    work.put("disableGG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("disableGB", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("disableBG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("allowGG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("allowGB", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("allowBG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferGG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferGB", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferBG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("requireGG", new Object[]{null, Boolean.TRUE});
    work.put("requireGB", new Object[]{null, Boolean.TRUE});
    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-caGG", new Object[]{null, Boolean.TRUE});
    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-fullGG", new Object[]{null, Boolean.TRUE});
    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
    expectedmap.put("sslhostsslcertgh8", work);
    work = (TreeMap) work.clone();
    expectedmap.put("sslhostsslcertgh9", work);

    work = (TreeMap) defaultexpected.clone();
    work.put("disableGG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("disableGB", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("disableBG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("allowGG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("allowGB", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("allowBG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferGG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferGB", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferBG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("requireGG", new Object[]{null, Boolean.TRUE});
    work.put("requireGB", new Object[]{null, Boolean.TRUE});
    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-caGG", new Object[]{null, Boolean.TRUE});
    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-fullGG", new Object[]{HOSTNAME, Boolean.TRUE});
    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
    expectedmap.put("sslhostsslcertbh8", work);
    work = (TreeMap) work.clone();
    expectedmap.put("sslhostsslcertbh9", work);

    work = (TreeMap) defaultexpected.clone();
    work.put("disableGG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("disableGB", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("disableBG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
    work.put("allowGG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("allowGB", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("allowBG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferGG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferGB", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("preferBG", new Object[]{SSLMODE, Boolean.TRUE});
    work.put("requireGG", new Object[]{null, Boolean.TRUE});
    work.put("requireGB", new Object[]{null, Boolean.TRUE});
    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-caGG", new Object[]{null, Boolean.TRUE});
    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
    work.put("verify-fullGG", new Object[]{null, Boolean.TRUE});
    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
    expectedmap.put("sslcertgh8", work);
    work = (TreeMap) work.clone();
    expectedmap.put("sslcertgh9", work);

    work = (TreeMap) work.clone();
    work.put("verify-fullGG", new Object[]{HOSTNAME, Boolean.TRUE});
    expectedmap.put("sslcertbh8", work);
    work = (TreeMap) work.clone();
    expectedmap.put("sslcertbh9", work);

  }


}
