require "../../spec_helper"
require "openssl"
require "../../../support/finalize"

class OpenSSL::SSL::Context
  include FinalizeCounter
end

describe OpenSSL::SSL::Context do
  it "new for client" do
    context = OpenSSL::SSL::Context::Client.new

    {% if OpenSSL.has_constant?(:KTLS) %}
      (context.options & OpenSSL::SSL::Options::ALL).should eq(OpenSSL::SSL::Options::ALL & ~OpenSSL::SSL::Options::ENABLE_KTLS)
    {% else %}
      (context.options & OpenSSL::SSL::Options::ALL).should eq(OpenSSL::SSL::Options::ALL)
    {% end %}
    (context.options & OpenSSL::SSL::Options::NO_SESSION_RESUMPTION_ON_RENEGOTIATION).should eq(OpenSSL::SSL::Options::NO_SESSION_RESUMPTION_ON_RENEGOTIATION)

    context.modes.should eq(OpenSSL::SSL::Modes.flags(AUTO_RETRY, RELEASE_BUFFERS))
    context.verify_mode.should eq(OpenSSL::SSL::VerifyMode::PEER)
  end

  it "new for server" do
    context = OpenSSL::SSL::Context::Server.new

    {% if OpenSSL.has_constant?(:KTLS) %}
      (context.options & OpenSSL::SSL::Options::ALL).should eq(OpenSSL::SSL::Options::ALL & ~OpenSSL::SSL::Options::ENABLE_KTLS)
    {% else %}
      (context.options & OpenSSL::SSL::Options::ALL).should eq(OpenSSL::SSL::Options::ALL)
    {% end %}
    (context.options & OpenSSL::SSL::Options::NO_SESSION_RESUMPTION_ON_RENEGOTIATION).should eq(OpenSSL::SSL::Options::NO_SESSION_RESUMPTION_ON_RENEGOTIATION)
    (context.options & OpenSSL::SSL::Options::NO_RENEGOTIATION).should eq(OpenSSL::SSL::Options::NO_RENEGOTIATION)

    context.modes.should eq(OpenSSL::SSL::Modes.flags(AUTO_RETRY, RELEASE_BUFFERS))
    context.verify_mode.should eq(OpenSSL::SSL::VerifyMode::NONE)
  end

  it "insecure for client" do
    context = OpenSSL::SSL::Context::Client.insecure
    context.should be_a(OpenSSL::SSL::Context::Client)
    context.verify_mode.should eq(OpenSSL::SSL::VerifyMode::NONE)
    context.options.no_ssl_v3?.should_not be_true
    context.modes.should eq(OpenSSL::SSL::Modes::AUTO_RETRY)
  end

  it "insecure for server" do
    context = OpenSSL::SSL::Context::Server.insecure
    context.should be_a(OpenSSL::SSL::Context::Server)
    context.verify_mode.should eq(OpenSSL::SSL::VerifyMode::NONE)
    context.options.no_ssl_v3?.should_not be_true
    context.modes.should eq(OpenSSL::SSL::Modes::AUTO_RETRY)
  end

  it "sets certificate chain" do
    context = OpenSSL::SSL::Context::Client.new
    context.certificate_chain = datapath("openssl", "openssl.crt")
  end

  it "fails to set certificate chain" do
    context = OpenSSL::SSL::Context::Client.new
    expect_raises(OpenSSL::Error) { context.certificate_chain = datapath("nonexistent.crt") }
    expect_raises(OpenSSL::Error) { context.certificate_chain = datapath("test_file.txt") }
  end

  it "sets private key" do
    context = OpenSSL::SSL::Context::Client.new
    context.private_key = datapath("openssl", "openssl.key")
  end

  it "fails to set private key" do
    context = OpenSSL::SSL::Context::Client.new
    expect_raises(OpenSSL::Error) { context.private_key = datapath("nonexistent.key") }
    expect_raises(OpenSSL::Error) { context.private_key = datapath("test_file.txt") }
  end

  describe "ciphers" do
    pending "uses intermediate default ciphers" do
      # Can't be checked because `Context#ciphers` is not implemented.
      OpenSSL::SSL::Context::Client.new.ciphers.should eq OpenSSL::SSL::Context::CIPHERS_OLD
      OpenSSL::SSL::Context::Server.new.ciphers.should eq OpenSSL::SSL::Context::CIPHERS_INTERMEDIATE
    end

    it "sets ciphers" do
      ciphers = "EDH+aRSA DES-CBC3-SHA !RC4"
      context = OpenSSL::SSL::Context::Client.new
      (context.ciphers = ciphers).should eq(ciphers)
    end

    it "sets cipher_suites" do
      cipher_suites = OpenSSL::SSL::Context::CIPHER_SUITES_MODERN
      context = OpenSSL::SSL::Context::Client.new
      (context.cipher_suites = cipher_suites).should eq(cipher_suites)
    end

    it "sets modern ciphers" do
      OpenSSL::SSL::Context::Client.new.set_modern_ciphers
    end

    it "sets intermediate ciphers" do
      OpenSSL::SSL::Context::Client.new.set_intermediate_ciphers
    end

    it "sets old ciphers" do
      OpenSSL::SSL::Context::Client.new.set_old_ciphers
    end
  end

  it "changes security level" do
    context = OpenSSL::SSL::Context::Client.new
    level = context.security_level
    context.security_level = level + 1

    if LibSSL.responds_to?(:ssl_ctx_set_security_level)
      context.security_level.should eq(level + 1)
    else
      context.security_level.should eq 0
    end
  end

  it "adds temporary ecdh curve (P-256)" do
    context = OpenSSL::SSL::Context::Client.new
    context.set_tmp_ecdh_key
  end

  it "adds options" do
    context = OpenSSL::SSL::Context::Client.new
    context.remove_options(context.options) # reset
    default_options = context.options       # options we can't unset

    context.add_options(OpenSSL::SSL::Options::ALL)
      .should eq(default_options | OpenSSL::SSL::Options::ALL)

    context.add_options(OpenSSL::SSL::Options.flags(NO_SSL_V2, NO_SSL_V3))
      .should eq(OpenSSL::SSL::Options.flags(ALL, NO_SSL_V2, NO_SSL_V3))
  end

  it "removes options" do
    context = OpenSSL::SSL::Context::Client.insecure
    default_options = context.options
    context.add_options(OpenSSL::SSL::Options.flags(NO_TLS_V1, NO_SSL_V2))
    context.remove_options(OpenSSL::SSL::Options::NO_TLS_V1).should eq(default_options | OpenSSL::SSL::Options::NO_SSL_V2)
  end

  it "returns options" do
    context = OpenSSL::SSL::Context::Client.insecure
    default_options = context.options
    context.add_options(OpenSSL::SSL::Options.flags(ALL, NO_SSL_V2))
    context.options.should eq(default_options | OpenSSL::SSL::Options.flags(ALL, NO_SSL_V2))
  end

  it "adds modes" do
    context = OpenSSL::SSL::Context::Client.insecure
    context.add_modes(OpenSSL::SSL::Modes::AUTO_RETRY).should eq(OpenSSL::SSL::Modes::AUTO_RETRY)
    context.add_modes(OpenSSL::SSL::Modes::RELEASE_BUFFERS)
      .should eq(OpenSSL::SSL::Modes.flags(AUTO_RETRY, RELEASE_BUFFERS))
  end

  it "removes modes" do
    context = OpenSSL::SSL::Context::Client.insecure
    context.add_modes(OpenSSL::SSL::Modes.flags(AUTO_RETRY, RELEASE_BUFFERS))
    context.remove_modes(OpenSSL::SSL::Modes::AUTO_RETRY).should eq(OpenSSL::SSL::Modes::RELEASE_BUFFERS)
  end

  it "returns modes" do
    context = OpenSSL::SSL::Context::Client.insecure
    context.add_modes(OpenSSL::SSL::Modes.flags(AUTO_RETRY, RELEASE_BUFFERS))
    context.modes.should eq(OpenSSL::SSL::Modes.flags(AUTO_RETRY, RELEASE_BUFFERS))
  end

  it "sets the verify mode" do
    context = OpenSSL::SSL::Context::Client.new
    context.verify_mode = OpenSSL::SSL::VerifyMode::NONE
    context.verify_mode.should eq(OpenSSL::SSL::VerifyMode::NONE)
    context.verify_mode = OpenSSL::SSL::VerifyMode::PEER
    context.verify_mode.should eq(OpenSSL::SSL::VerifyMode::PEER)
  end

  if LibSSL.responds_to?(:ssl_ctx_set_alpn_protos)
    it "alpn_protocol=" do
      context = OpenSSL::SSL::Context::Client.insecure
      context.alpn_protocol = "h2"
    end
  end

  it "on_server_name" do
    context = OpenSSL::SSL::Context::Server.new
    context.certificate_chain = datapath("openssl", "openssl.crt")
    context.private_key = datapath("openssl", "openssl.key")

    alt_context = OpenSSL::SSL::Context::Server.new
    alt_context.certificate_chain = datapath("openssl", "openssl.crt")
    alt_context.private_key = datapath("openssl", "openssl.key")

    context.on_server_name do |hostname|
      if hostname == "alt.example.com"
        alt_context
      else
        nil
      end
    end
  end

  it "calls #finalize on insecure client context" do
    assert_finalizes("insecure_client_ctx") { OpenSSL::SSL::Context::Client.insecure }
  end

  it "calls #finalize on insecure server context" do
    assert_finalizes("insecure_server_ctx") { OpenSSL::SSL::Context::Server.insecure }
  end

  describe ".from_hash" do
    it "builds" do
      private_key = datapath("openssl", "openssl.key")
      certificate = datapath("openssl", "openssl.crt")

      context = OpenSSL::SSL::Context::Client.from_hash({"key" => private_key, "cert" => certificate, "verify_mode" => "none"})
      context.verify_mode.should eq OpenSSL::SSL::VerifyMode::NONE

      context = OpenSSL::SSL::Context::Server.from_hash({"key" => private_key, "cert" => certificate})
      context.verify_mode.should eq OpenSSL::SSL::VerifyMode::NONE

      context = OpenSSL::SSL::Context::Client.from_hash({"key" => private_key, "cert" => certificate, "ca" => certificate})
      context.verify_mode.should eq OpenSSL::SSL::VerifyMode::PEER

      context = OpenSSL::SSL::Context::Client.from_hash({"key" => private_key, "cert" => certificate, "ca" => File.dirname(certificate)})
      context.verify_mode.should eq OpenSSL::SSL::VerifyMode::PEER
    end

    it "errors" do
      private_key = datapath("openssl", "openssl.key")
      certificate = datapath("openssl", "openssl.crt")
      nonexistent = datapath("openssl", "nonexistent")

      expect_raises(ArgumentError, "missing private key") do
        OpenSSL::SSL::Context::Client.from_hash({} of String => String)
      end
      expect_raises(OpenSSL::Error, /SSL_CTX_use_PrivateKey_file: error:.*:No such file or directory/) do
        OpenSSL::SSL::Context::Client.from_hash({"key" => nonexistent})
      end
      expect_raises(ArgumentError, "missing certificate") do
        OpenSSL::SSL::Context::Client.from_hash({"key" => private_key})
      end
      expect_raises(OpenSSL::Error, /SSL_CTX_use_certificate_chain_file: error:.*:No such file or directory/) do
        OpenSSL::SSL::Context::Client.from_hash({"key" => private_key, "cert" => nonexistent})
      end
      expect_raises(ArgumentError, "Invalid SSL context: missing CA certificate") do
        OpenSSL::SSL::Context::Client.from_hash({"key" => private_key, "cert" => certificate})
      end
      expect_raises(ArgumentError, %(Invalid SSL context: unknown verify mode "foo")) do
        OpenSSL::SSL::Context::Client.from_hash({"key" => private_key, "cert" => certificate, "verify_mode" => "foo"})
      end
      expect_raises(ArgumentError, "Invalid SSL context: missing CA certificate") do
        OpenSSL::SSL::Context::Client.from_hash({"key" => private_key, "cert" => certificate, "verify_mode" => "peer"})
      end
      expect_raises(OpenSSL::Error, /SSL_CTX_load_verify_locations: error:.*:No such file or directory/) do
        OpenSSL::SSL::Context::Client.from_hash({"key" => private_key, "cert" => certificate, "ca" => nonexistent})
      end
    end
  end

  describe OpenSSL::SSL::VerifyMode do
    it ".parse none (#7455)" do
      OpenSSL::SSL::VerifyMode.parse("none").should eq OpenSSL::SSL::VerifyMode::NONE
    end
  end
end
