// Copyright 2014 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.

#include "net/quic/crypto/channel_id_chromium.h"

#include <utility>
#include <vector>

#include "base/strings/string_util.h"
#include "crypto/ec_private_key.h"
#include "crypto/ec_signature_creator.h"
#include "net/base/net_errors.h"
#include "net/cert/asn1_util.h"
#include "net/ssl/channel_id_service.h"

namespace net {

ChannelIDKeyChromium::ChannelIDKeyChromium(
    std::unique_ptr<crypto::ECPrivateKey> ec_private_key)
    : ec_private_key_(std::move(ec_private_key)) {}

ChannelIDKeyChromium::~ChannelIDKeyChromium() {}

bool ChannelIDKeyChromium::Sign(quic::QuicStringPiece signed_data,
                                std::string* out_signature) const {
  std::unique_ptr<crypto::ECSignatureCreator> sig_creator(
      crypto::ECSignatureCreator::Create(ec_private_key_.get()));
  if (!sig_creator) {
    return false;
  }
  const size_t len1 = strlen(quic::ChannelIDVerifier::kContextStr) + 1;
  const size_t len2 = strlen(quic::ChannelIDVerifier::kClientToServerStr) + 1;
  std::vector<uint8_t> data(len1 + len2 + signed_data.size());
  memcpy(&data[0], quic::ChannelIDVerifier::kContextStr, len1);
  memcpy(&data[len1], quic::ChannelIDVerifier::kClientToServerStr, len2);
  memcpy(&data[len1 + len2], signed_data.data(), signed_data.size());
  std::vector<uint8_t> der_signature;
  if (!sig_creator->Sign(&data[0], data.size(), &der_signature)) {
    return false;
  }
  std::vector<uint8_t> raw_signature;
  if (!sig_creator->DecodeSignature(der_signature, &raw_signature)) {
    return false;
  }
  memcpy(base::WriteInto(out_signature, raw_signature.size() + 1),
         &raw_signature[0], raw_signature.size());
  return true;
}

std::string ChannelIDKeyChromium::SerializeKey() const {
  std::string out_key;
  if (!ec_private_key_->ExportRawPublicKey(&out_key)) {
    return std::string();
  }
  return out_key;
}

// A Job handles the lookup of a single channel ID.  It is owned by the
// quic::ChannelIDSource. If the operation can not complete synchronously, it
// will notify the quic::ChannelIDSource upon completion.
class ChannelIDSourceChromium::Job {
 public:
  Job(ChannelIDSourceChromium* channel_id_source,
      ChannelIDService* channel_id_service);

  // Starts the channel ID lookup.  If |quic::QUIC_PENDING| is returned, then
  // |callback| will be invoked asynchronously when the operation completes.
  quic::QuicAsyncStatus GetChannelIDKey(
      const std::string& hostname,
      std::unique_ptr<quic::ChannelIDKey>* channel_id_key,
      quic::ChannelIDSourceCallback* callback);

 private:
  enum State {
    STATE_NONE,
    STATE_GET_CHANNEL_ID_KEY,
    STATE_GET_CHANNEL_ID_KEY_COMPLETE,
  };

  int DoLoop(int last_io_result);
  void OnIOComplete(int result);
  int DoGetChannelIDKey(int result);
  int DoGetChannelIDKeyComplete(int result);

  // Channel ID source to notify when this jobs completes.
  ChannelIDSourceChromium* const channel_id_source_;

  ChannelIDService* const channel_id_service_;

  std::unique_ptr<crypto::ECPrivateKey> channel_id_crypto_key_;
  ChannelIDService::Request channel_id_request_;

  // |hostname| specifies the hostname for which we need a channel ID.
  std::string hostname_;

  std::unique_ptr<quic::ChannelIDSourceCallback> callback_;

  std::unique_ptr<quic::ChannelIDKey> channel_id_key_;

  State next_state_;

  DISALLOW_COPY_AND_ASSIGN(Job);
};

ChannelIDSourceChromium::Job::Job(ChannelIDSourceChromium* channel_id_source,
                                  ChannelIDService* channel_id_service)
    : channel_id_source_(channel_id_source),
      channel_id_service_(channel_id_service),
      next_state_(STATE_NONE) {}

quic::QuicAsyncStatus ChannelIDSourceChromium::Job::GetChannelIDKey(
    const std::string& hostname,
    std::unique_ptr<quic::ChannelIDKey>* channel_id_key,
    quic::ChannelIDSourceCallback* callback) {
  DCHECK(channel_id_key);
  DCHECK(callback);

  if (STATE_NONE != next_state_) {
    DLOG(DFATAL) << "GetChannelIDKey has begun";
    return quic::QUIC_FAILURE;
  }

  channel_id_key_.reset();

  hostname_ = hostname;

  next_state_ = STATE_GET_CHANNEL_ID_KEY;
  switch (DoLoop(OK)) {
    case OK:
      *channel_id_key = std::move(channel_id_key_);
      return quic::QUIC_SUCCESS;
    case ERR_IO_PENDING:
      callback_.reset(callback);
      return quic::QUIC_PENDING;
    default:
      channel_id_key->reset();
      return quic::QUIC_FAILURE;
  }
}

int ChannelIDSourceChromium::Job::DoLoop(int last_result) {
  int rv = last_result;
  do {
    State state = next_state_;
    next_state_ = STATE_NONE;
    switch (state) {
      case STATE_GET_CHANNEL_ID_KEY:
        DCHECK(rv == OK);
        rv = DoGetChannelIDKey(rv);
        break;
      case STATE_GET_CHANNEL_ID_KEY_COMPLETE:
        rv = DoGetChannelIDKeyComplete(rv);
        break;
      case STATE_NONE:
      default:
        rv = ERR_UNEXPECTED;
        LOG(DFATAL) << "unexpected state " << state;
        break;
    }
  } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE);
  return rv;
}

void ChannelIDSourceChromium::Job::OnIOComplete(int result) {
  int rv = DoLoop(result);
  if (rv != ERR_IO_PENDING) {
    std::unique_ptr<quic::ChannelIDSourceCallback> callback(
        callback_.release());
    callback->Run(&channel_id_key_);
    // Will delete |this|.
    channel_id_source_->OnJobComplete(this);
  }
}

int ChannelIDSourceChromium::Job::DoGetChannelIDKey(int result) {
  next_state_ = STATE_GET_CHANNEL_ID_KEY_COMPLETE;

  return channel_id_service_->GetOrCreateChannelID(
      hostname_, &channel_id_crypto_key_,
      base::Bind(&ChannelIDSourceChromium::Job::OnIOComplete,
                 base::Unretained(this)),
      &channel_id_request_);
}

int ChannelIDSourceChromium::Job::DoGetChannelIDKeyComplete(int result) {
  DCHECK_EQ(STATE_NONE, next_state_);
  if (result != OK) {
    DLOG(WARNING) << "Failed to look up channel ID: " << ErrorToString(result);
    return result;
  }

  DCHECK(channel_id_crypto_key_);
  channel_id_key_.reset(
      new ChannelIDKeyChromium(std::move(channel_id_crypto_key_)));
  return result;
}

ChannelIDSourceChromium::ChannelIDSourceChromium(
    ChannelIDService* channel_id_service)
    : channel_id_service_(channel_id_service) {}

ChannelIDSourceChromium::~ChannelIDSourceChromium() {}

quic::QuicAsyncStatus ChannelIDSourceChromium::GetChannelIDKey(
    const std::string& hostname,
    std::unique_ptr<quic::ChannelIDKey>* channel_id_key,
    quic::ChannelIDSourceCallback* callback) {
  std::unique_ptr<Job> job = std::make_unique<Job>(this, channel_id_service_);
  quic::QuicAsyncStatus status =
      job->GetChannelIDKey(hostname, channel_id_key, callback);
  if (status == quic::QUIC_PENDING) {
    Job* job_ptr = job.get();
    active_jobs_[job_ptr] = std::move(job);
  }
  return status;
}

void ChannelIDSourceChromium::OnJobComplete(Job* job) {
  active_jobs_.erase(job);
}

}  // namespace net
