// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef DEVICE_FIDO_FIDO_REQUEST_HANDLER_H_
#define DEVICE_FIDO_FIDO_REQUEST_HANDLER_H_

#include "device/fido/fido_request_handler_base.h"

#include <utility>

#include "base/callback.h"
#include "base/containers/flat_set.h"
#include "base/macros.h"
#include "base/optional.h"
#include "device/fido/fido_authenticator.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_device.h"
#include "device/fido/fido_transport_protocol.h"

namespace device {

// Handles receiving response form potentially multiple connected authenticators
// and relaying response to the relying party.
template <class Response>
class FidoRequestHandler : public FidoRequestHandlerBase {
 public:
  using CompletionCallback =
      base::OnceCallback<void(FidoReturnCode status_code,
                              base::Optional<Response> response_data)>;

  FidoRequestHandler(service_manager::Connector* connector,
                     const base::flat_set<FidoTransportProtocol>& transports,
                     CompletionCallback completion_callback)
      : FidoRequestHandler(connector,
                           transports,
                           std::move(completion_callback),
                           AddPlatformAuthenticatorCallback()) {}
  FidoRequestHandler(
      service_manager::Connector* connector,
      const base::flat_set<FidoTransportProtocol>& transports,
      CompletionCallback completion_callback,
      AddPlatformAuthenticatorCallback add_platform_authenticator)
      : FidoRequestHandlerBase(connector,
                               transports,
                               std::move(add_platform_authenticator)),
        completion_callback_(std::move(completion_callback)) {}
  ~FidoRequestHandler() override {
    if (!is_complete())
      CancelOngoingTasks();
  }

  bool is_complete() const { return completion_callback_.is_null(); }

 protected:
  // Converts authenticator response code received from CTAP1/CTAP2 device into
  // FidoReturnCode and passes response data to webauth::mojom::Authenticator.
  void OnAuthenticatorResponse(FidoAuthenticator* authenticator,
                               CtapDeviceResponseCode device_response_code,
                               base::Optional<Response> response_data) {
    if (is_complete()) {
      DVLOG(2)
          << "Response from authenticator received after request is complete.";
      return;
    }

    const auto return_code = ConvertDeviceResponseCodeToFidoReturnCode(
        device_response_code, response_data.has_value());

    // Any authenticator response codes that do not result from user consent
    // imply that the authenticator should be dropped and that other on-going
    // requests should continue until timeout is reached.
    if (!return_code) {
      active_authenticators().erase(authenticator->GetId());
      return;
    }

    // Once response has been passed to the relying party, cancel all other on
    // going requests.
    CancelOngoingTasks(authenticator->GetId());
    std::move(completion_callback_).Run(*return_code, std::move(response_data));
  }

 private:
  static base::Optional<FidoReturnCode>
  ConvertDeviceResponseCodeToFidoReturnCode(
      CtapDeviceResponseCode device_response_code,
      bool response_has_value) {
    switch (device_response_code) {
      case CtapDeviceResponseCode::kSuccess:
        return response_has_value
                   ? FidoReturnCode::kSuccess
                   : FidoReturnCode::kAuthenticatorResponseInvalid;

      // These errors are only returned after the user interacted with the
      // authenticator.
      case CtapDeviceResponseCode::kCtap2ErrCredentialExcluded:
        return FidoReturnCode::kUserConsentButCredentialExcluded;
      case CtapDeviceResponseCode::kCtap2ErrNoCredentials:
        return FidoReturnCode::kUserConsentButCredentialNotRecognized;

      // This error is returned by some authenticators (e.g. the "Yubico FIDO
      // 2" CTAP2 USB keys) during GetAssertion **before the user interacted
      // with the device**. The authenticator does this to avoid blinking (and
      // possibly asking the user for their PIN) for requests it knows
      // beforehand it cannot handle.
      //
      // Ignore this error to avoid canceling the request without user
      // interaction.
      case CtapDeviceResponseCode::kCtap2ErrInvalidCredential:
        return base::nullopt;

      default:
        return base::nullopt;
    }
  }

  CompletionCallback completion_callback_;

  DISALLOW_COPY_AND_ASSIGN(FidoRequestHandler);
};

}  // namespace device

#endif  // DEVICE_FIDO_FIDO_REQUEST_HANDLER_H_
