# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

require 'openssl'

require 'logstash/util'
require 'logstash/webserver'
require "stud/try"
require "manticore"

describe 'api webserver', :skip_fips do
  let!(:logger) { double("Logger").as_null_object }
  let!(:agent) { double("Agent").as_null_object }
  subject(:webserver) { LogStash::WebServer.new(logger, agent, webserver_options) }

  let(:webserver_options) { Hash.new }

  # provide a shared context to take care of ensuring that the webserver
  # is running during the spec runs of examples in the including context
  shared_context 'running webserver' do
    let(:webserver) { defined?(super()) ? super() : fail("included context requires `webserver` to be present") }

    # since we are running the webserver on another
    # thread, ensure that a crash doesn't go unnoticed.
    around(:each) do |example|
      abort = Thread.abort_on_exception
      example.call
      Thread.abort_on_exception = abort
    end

    # If webmock is active, allow real network connections
    before(:each) { WebMock.allow_net_connect! if defined?(WebMock) }

    # ensure our API webserver is running with the provided config
    # before running our specs that validate responses
    let!(:webserver_thread) { Thread.new(webserver, &:run) }
    before(:each) do
      Stud.try(10.times) { fail('API WebServer not running yet...') unless webserver.port }
    end
    after(:each) do
      webserver.stop
      webserver_thread.join
    end
  end

  context "when configured with api.ssl.supported_protocols" do
    let(:ca_file) { File.join(certs_path, "root.crt") }
    let(:certs_path) { File.expand_path("../../fixtures/webserver_certs/generated", __FILE__) }
    let(:keystore_path) { File.join(certs_path,  "server_from_root.p12") }
    let(:keystore_password) { "12345678" }
    let(:supported_protocols) { %w[TLSv1.3] }
    let(:ssl_params) { {:supported_protocols => supported_protocols, :keystore_path => keystore_path, :keystore_password => LogStash::Util::Password.new(keystore_password)} }
    let(:webserver_options) { super().merge(:ssl_params => ssl_params) }
    let(:client_protocols) { nil }
    let(:client) { Manticore::Client.new(ssl: { ca_file: ca_file, protocols: client_protocols }) }
    let(:response) { client.get("https://127.0.0.1:#{webserver.port}") }

    include_context 'running webserver'

    context 'an HTTPS request using TLSv1.3' do
      let(:client_protocols) { %w[TLSv1.3] }
      it 'succeeds' do
        expect(response.code).to eq(200)
      end
    end

    context 'an HTTPS request using TLSv1.2' do
      let(:client_protocols) { %w[TLSv1.2] }
      it 'fails' do
        expect { response.code }.to raise_error(Manticore::ClientProtocolException, a_string_including("handshake"))
      end
    end
  end

  %w(
      server_from_root.p12
      server_from_intermediate.p12
      server_from_root.jks
      server_from_intermediate.jks
    ).each do |keystore_name|
    context "when configured with keystore #{keystore_name}" do
      let(:ca_file) { File.join(certs_path, "root.crt") }
      let(:certs_path) { File.expand_path("../../fixtures/webserver_certs/generated", __FILE__) }
      let(:keystore_path) { File.join(certs_path, "#{keystore_name}") }
      let(:keystore_password) { "12345678" }

      let(:ssl_params) { {:keystore_path => keystore_path, :keystore_password => LogStash::Util::Password.new(keystore_password)} }
      let(:webserver_options) { super().merge(:ssl_params => ssl_params) }

      context 'and invalid credentials' do
        let(:keystore_password) { "wrong" }
        it 'raises a helpful error' do
          expect { webserver }.to raise_error(ArgumentError, a_string_including("keystore password was incorrect"))
        end
      end

      context "when started" do
        include_context 'running webserver'

        context 'an HTTPS request' do
          it 'succeeds' do
            client = Manticore::Client.new(ssl: { ca_file: ca_file })
            response = client.get("https://127.0.0.1:#{webserver.port}")
            expect(response.code).to eq(200)
          end

          # this is mostly a sanity check for our testing methodology
          # If this fails, we cannot trust success from the other specs
          context 'without providing CA' do
            it 'fails' do
              client = Manticore::Client.new(ssl: { })
              expect do
                client.get("https://127.0.0.1:#{webserver.port}").code
              end.to raise_error(Manticore::ClientProtocolException, a_string_including("unable to find valid certification path to requested target"))
            end
          end
        end

        context 'an HTTP request' do
          it 'fails' do
            client = Manticore::Client.new
            expect do
              client.get("http://127.0.0.1:#{webserver.port}").code
            end.to raise_error(Manticore::ClientProtocolException, a_string_including("failed to respond"))
          end
        end
      end
    end
  end
end
