/*
 * nghttp2 - HTTP/2 C Library
 *
 * Copyright (c) 2021 Tatsuhiro Tsujikawa
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#include "shrpx_quic_connection_handler.h"

#include <openssl/rand.h>

#include <ngtcp2/ngtcp2.h>
#include <ngtcp2/ngtcp2_crypto.h>

#include "shrpx_worker.h"
#include "shrpx_client_handler.h"
#include "shrpx_log.h"
#include "shrpx_http3_upstream.h"
#include "shrpx_connection_handler.h"

namespace shrpx {

namespace {
void stateless_reset_bucket_regen_timercb(struct ev_loop *loop, ev_timer *w,
                                          int revents) {
  auto quic_conn_handler = static_cast<QUICConnectionHandler *>(w->data);

  quic_conn_handler->on_stateless_reset_bucket_regen();
}
} // namespace

QUICConnectionHandler::QUICConnectionHandler(Worker *worker)
    : worker_{worker},
      stateless_reset_bucket_{SHRPX_QUIC_STATELESS_RESET_BURST} {
  ev_timer_init(&stateless_reset_bucket_regen_timer_,
                stateless_reset_bucket_regen_timercb, 0., 1.);
  stateless_reset_bucket_regen_timer_.data = this;
}

QUICConnectionHandler::~QUICConnectionHandler() {
  ev_timer_stop(worker_->get_loop(), &stateless_reset_bucket_regen_timer_);
}

int QUICConnectionHandler::handle_packet(const UpstreamAddr *faddr,
                                         const Address &remote_addr,
                                         const Address &local_addr,
                                         const uint8_t *data, size_t datalen) {
  int rv;
  uint32_t version;
  const uint8_t *dcid, *scid;
  size_t dcidlen, scidlen;

  rv = ngtcp2_pkt_decode_version_cid(&version, &dcid, &dcidlen, &scid, &scidlen,
                                     data, datalen, SHRPX_QUIC_SCIDLEN);
  switch (rv) {
  case 0:
    break;
  case NGTCP2_ERR_VERSION_NEGOTIATION:
    send_version_negotiation(faddr, version, dcid, dcidlen, scid, scidlen,
                             remote_addr, local_addr);

    return 0;
  default:
    return 0;
  }

  auto config = get_config();

  ngtcp2_cid dcid_key;
  ngtcp2_cid_init(&dcid_key, dcid, dcidlen);

  auto conn_handler = worker_->get_connection_handler();

  ClientHandler *handler;

  auto &quicconf = config->quic;

  auto it = connections_.find(dcid_key);
  if (it == std::end(connections_)) {
    auto cwit = close_waits_.find(dcid_key);
    if (cwit != std::end(close_waits_)) {
      auto cw = (*cwit).second;

      cw->handle_packet(faddr, remote_addr, local_addr, data, datalen);

      return 0;
    }

    if (data[0] & 0x80) {
      if (generate_quic_hashed_connection_id(dcid_key, remote_addr, local_addr,
                                             dcid_key) != 0) {
        return 0;
      }

      it = connections_.find(dcid_key);
      if (it == std::end(connections_)) {
        auto cwit = close_waits_.find(dcid_key);
        if (cwit != std::end(close_waits_)) {
          auto cw = (*cwit).second;

          cw->handle_packet(faddr, remote_addr, local_addr, data, datalen);

          return 0;
        }
      }
    }
  }

  if (it == std::end(connections_)) {
    std::array<uint8_t, SHRPX_QUIC_DECRYPTED_DCIDLEN> decrypted_dcid;

    if (dcidlen == SHRPX_QUIC_SCIDLEN) {
      if (decrypt_quic_connection_id(
              decrypted_dcid.data(), dcid,
              quicconf.upstream.cid_encryption_key.data()) != 0) {
        return 0;
      }

      if (!std::equal(std::begin(decrypted_dcid),
                      std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN,
                      worker_->get_cid_prefix())) {
        auto quic_lwp =
            conn_handler->match_quic_lingering_worker_process_cid_prefix(
                decrypted_dcid.data(), decrypted_dcid.size());
        if (quic_lwp) {
          if (conn_handler->forward_quic_packet_to_lingering_worker_process(
                  quic_lwp, remote_addr, local_addr, data, datalen) == 0) {
            return 0;
          }

          return 0;
        }
      }
    }

    // new connection

    auto &upstreamconf = config->conn.upstream;
    if (worker_->get_worker_stat()->num_connections >=
        upstreamconf.worker_connections) {
      if (LOG_ENABLED(INFO)) {
        LOG(INFO) << "Too many connections >="
                  << upstreamconf.worker_connections;
      }

      return 0;
    }

    ngtcp2_pkt_hd hd;
    ngtcp2_cid odcid, *podcid = nullptr;
    const uint8_t *token = nullptr;
    size_t tokenlen = 0;

    switch (ngtcp2_accept(&hd, data, datalen)) {
    case 0: {
      // If we get Initial and it has the CID prefix of this worker, it
      // is likely that client is intentionally use the our prefix.
      // Just drop it.
      if (dcidlen == SHRPX_QUIC_SCIDLEN &&
          std::equal(std::begin(decrypted_dcid),
                     std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN,
                     worker_->get_cid_prefix())) {
        return 0;
      }

      if (hd.token.len == 0) {
        if (quicconf.upstream.require_token) {
          send_retry(faddr, version, dcid, dcidlen, scid, scidlen, remote_addr,
                     local_addr);

          return 0;
        }

        break;
      }

      auto &quic_secret = worker_->get_quic_secret();
      auto &secret = quic_secret->token_secret;

      switch (hd.token.base[0]) {
      case NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY:
        if (verify_retry_token(&odcid, hd.token.base, hd.token.len, &hd.dcid,
                               &remote_addr.su.sa, remote_addr.len,
                               secret.data()) != 0) {
          if (LOG_ENABLED(INFO)) {
            LOG(INFO) << "Failed to validate Retry token from remote="
                      << util::to_numeric_addr(&remote_addr);
          }

          // 2nd Retry packet is not allowed, so send CONNECTION_CLOSE
          // with INVALID_TOKEN.
          send_connection_close(faddr, version, &hd.dcid, &hd.scid, remote_addr,
                                local_addr, NGTCP2_INVALID_TOKEN);
          return 0;
        }

        if (LOG_ENABLED(INFO)) {
          LOG(INFO) << "Successfully validated Retry token from remote="
                    << util::to_numeric_addr(&remote_addr);
        }

        podcid = &odcid;
        token = hd.token.base;
        tokenlen = hd.token.len;

        break;
      case NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR:
        if (verify_token(hd.token.base, hd.token.len, &remote_addr.su.sa,
                         remote_addr.len, secret.data()) != 0) {
          if (LOG_ENABLED(INFO)) {
            LOG(INFO) << "Failed to validate token from remote="
                      << util::to_numeric_addr(&remote_addr);
          }

          if (quicconf.upstream.require_token) {
            send_retry(faddr, version, dcid, dcidlen, scid, scidlen,
                       remote_addr, local_addr);

            return 0;
          }

          break;
        }

        if (LOG_ENABLED(INFO)) {
          LOG(INFO) << "Successfully validated token from remote="
                    << util::to_numeric_addr(&remote_addr);
        }

        token = hd.token.base;
        tokenlen = hd.token.len;

        break;
      default:
        if (quicconf.upstream.require_token) {
          send_retry(faddr, version, dcid, dcidlen, scid, scidlen, remote_addr,
                     local_addr);

          return 0;
        }

        break;
      }

      break;
    }
    case NGTCP2_ERR_RETRY:
      send_retry(faddr, version, dcid, dcidlen, scid, scidlen, remote_addr,
                 local_addr);
      return 0;
    case NGTCP2_ERR_VERSION_NEGOTIATION:
      send_version_negotiation(faddr, version, dcid, dcidlen, scid, scidlen,
                               remote_addr, local_addr);
      return 0;
    default:
      if (!config->single_thread && !(data[0] & 0x80) &&
          dcidlen == SHRPX_QUIC_SCIDLEN &&
          !std::equal(std::begin(decrypted_dcid),
                      std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN,
                      worker_->get_cid_prefix())) {
        if (conn_handler->forward_quic_packet(faddr, remote_addr, local_addr,
                                              decrypted_dcid.data(), data,
                                              datalen) == 0) {
          return 0;
        }
      }

      if (!(data[0] & 0x80)) {
        // TODO Must be rate limited
        send_stateless_reset(faddr, dcid, dcidlen, remote_addr, local_addr);
      }

      return 0;
    }

    handler = handle_new_connection(faddr, remote_addr, local_addr, hd, podcid,
                                    token, tokenlen);
    if (handler == nullptr) {
      return 0;
    }
  } else {
    handler = (*it).second;
  }

  if (handler->read_quic(faddr, remote_addr, local_addr, data, datalen) != 0) {
    delete handler;
    return 0;
  }

  handler->signal_write();

  return 0;
}

ClientHandler *QUICConnectionHandler::handle_new_connection(
    const UpstreamAddr *faddr, const Address &remote_addr,
    const Address &local_addr, const ngtcp2_pkt_hd &hd, const ngtcp2_cid *odcid,
    const uint8_t *token, size_t tokenlen) {
  std::array<char, NI_MAXHOST> host;
  std::array<char, NI_MAXSERV> service;
  int rv;

  rv = getnameinfo(&remote_addr.su.sa, remote_addr.len, host.data(),
                   host.size(), service.data(), service.size(),
                   NI_NUMERICHOST | NI_NUMERICSERV);
  if (rv != 0) {
    LOG(ERROR) << "getnameinfo() failed: " << gai_strerror(rv);

    return nullptr;
  }

  auto ssl_ctx = worker_->get_quic_sv_ssl_ctx();

  assert(ssl_ctx);

  auto ssl = tls::create_ssl(ssl_ctx);
  if (ssl == nullptr) {
    return nullptr;
  }

  assert(SSL_is_quic(ssl));

  SSL_set_accept_state(ssl);

  auto config = get_config();
  auto &quicconf = config->quic;

  if (quicconf.upstream.early_data) {
    SSL_set_quic_early_data_enabled(ssl, 1);
  }

  // Disable TLS session ticket if we don't have working ticket
  // keys.
  if (!worker_->get_ticket_keys()) {
    SSL_set_options(ssl, SSL_OP_NO_TICKET);
  }

  auto handler = std::make_unique<ClientHandler>(
      worker_, faddr->fd, ssl, StringRef{host.data()},
      StringRef{service.data()}, remote_addr.su.sa.sa_family, faddr);

  auto upstream = std::make_unique<Http3Upstream>(handler.get());
  if (upstream->init(faddr, remote_addr, local_addr, hd, odcid, token,
                     tokenlen) != 0) {
    return nullptr;
  }

  handler->setup_http3_upstream(std::move(upstream));

  return handler.release();
}

namespace {
uint32_t generate_reserved_version(const Address &addr, uint32_t version) {
  uint32_t h = 0x811C9DC5u;
  const uint8_t *p = reinterpret_cast<const uint8_t *>(&addr.su.sa);
  const uint8_t *ep = p + addr.len;

  for (; p != ep; ++p) {
    h ^= *p;
    h *= 0x01000193u;
  }

  version = htonl(version);
  p = (const uint8_t *)&version;
  ep = p + sizeof(version);

  for (; p != ep; ++p) {
    h ^= *p;
    h *= 0x01000193u;
  }

  h &= 0xf0f0f0f0u;
  h |= 0x0a0a0a0au;

  return h;
}
} // namespace

int QUICConnectionHandler::send_retry(
    const UpstreamAddr *faddr, uint32_t version, const uint8_t *ini_dcid,
    size_t ini_dcidlen, const uint8_t *ini_scid, size_t ini_scidlen,
    const Address &remote_addr, const Address &local_addr) {
  std::array<char, NI_MAXHOST> host;
  std::array<char, NI_MAXSERV> port;

  if (getnameinfo(&remote_addr.su.sa, remote_addr.len, host.data(), host.size(),
                  port.data(), port.size(),
                  NI_NUMERICHOST | NI_NUMERICSERV) != 0) {
    return -1;
  }

  ngtcp2_cid retry_scid;

  retry_scid.datalen = SHRPX_QUIC_SCIDLEN;
  // We do not steer packet based on Retry CID.
  if (RAND_bytes(retry_scid.data, retry_scid.datalen) != 1) {
    return -1;
  }

  std::array<uint8_t, NGTCP2_CRYPTO_MAX_RETRY_TOKENLEN> token;
  size_t tokenlen;

  ngtcp2_cid idcid, iscid;
  ngtcp2_cid_init(&idcid, ini_dcid, ini_dcidlen);
  ngtcp2_cid_init(&iscid, ini_scid, ini_scidlen);

  auto &quic_secret = worker_->get_quic_secret();
  auto &secret = quic_secret->token_secret;

  if (generate_retry_token(token.data(), tokenlen, &remote_addr.su.sa,
                           remote_addr.len, &retry_scid, &idcid,
                           secret.data()) != 0) {
    return -1;
  }

  std::array<uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> buf;

  auto nwrite =
      ngtcp2_crypto_write_retry(buf.data(), buf.size(), version, &iscid,
                                &retry_scid, &idcid, token.data(), tokenlen);
  if (nwrite < 0) {
    LOG(ERROR) << "ngtcp2_crypto_write_retry: " << ngtcp2_strerror(nwrite);
    return -1;
  }

  return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len,
                          &local_addr.su.sa, local_addr.len, buf.data(), nwrite,
                          0);
}

int QUICConnectionHandler::send_version_negotiation(
    const UpstreamAddr *faddr, uint32_t version, const uint8_t *ini_dcid,
    size_t ini_dcidlen, const uint8_t *ini_scid, size_t ini_scidlen,
    const Address &remote_addr, const Address &local_addr) {
  std::array<uint32_t, 2> sv;

  sv[0] = generate_reserved_version(remote_addr, version);
  sv[1] = NGTCP2_PROTO_VER_V1;

  std::array<uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> buf;

  uint8_t rand_byte;
  util::random_bytes(&rand_byte, &rand_byte + 1, worker_->get_randgen());

  auto nwrite = ngtcp2_pkt_write_version_negotiation(
      buf.data(), buf.size(), rand_byte, ini_scid, ini_scidlen, ini_dcid,
      ini_dcidlen, sv.data(), sv.size());
  if (nwrite < 0) {
    LOG(ERROR) << "ngtcp2_pkt_write_version_negotiation: "
               << ngtcp2_strerror(nwrite);
    return -1;
  }

  return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len,
                          &local_addr.su.sa, local_addr.len, buf.data(), nwrite,
                          0);
}

int QUICConnectionHandler::send_stateless_reset(const UpstreamAddr *faddr,
                                                const uint8_t *dcid,
                                                size_t dcidlen,
                                                const Address &remote_addr,
                                                const Address &local_addr) {
  if (stateless_reset_bucket_ == 0) {
    if (LOG_ENABLED(INFO)) {
      LOG(INFO) << "Stateless Reset bucket has been depleted";
    }

    return 0;
  }

  --stateless_reset_bucket_;

  if (!ev_is_active(&stateless_reset_bucket_regen_timer_)) {
    ev_timer_again(worker_->get_loop(), &stateless_reset_bucket_regen_timer_);
  }

  int rv;
  std::array<uint8_t, NGTCP2_STATELESS_RESET_TOKENLEN> token;
  ngtcp2_cid cid;

  ngtcp2_cid_init(&cid, dcid, dcidlen);

  auto &quic_secret = worker_->get_quic_secret();
  auto &secret = quic_secret->stateless_reset_secret;

  rv = generate_quic_stateless_reset_token(token.data(), &cid, secret.data(),
                                           secret.size());
  if (rv != 0) {
    return -1;
  }

  std::array<uint8_t, NGTCP2_MIN_STATELESS_RESET_RANDLEN> rand_bytes;

  if (RAND_bytes(rand_bytes.data(), rand_bytes.size()) != 1) {
    return -1;
  }

  std::array<uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> buf;

  auto nwrite =
      ngtcp2_pkt_write_stateless_reset(buf.data(), buf.size(), token.data(),
                                       rand_bytes.data(), rand_bytes.size());
  if (nwrite < 0) {
    LOG(ERROR) << "ngtcp2_pkt_write_stateless_reset: "
               << ngtcp2_strerror(nwrite);
    return -1;
  }

  if (LOG_ENABLED(INFO)) {
    LOG(INFO) << "Send stateless_reset to remote="
              << util::to_numeric_addr(&remote_addr)
              << " dcid=" << util::format_hex(dcid, dcidlen);
  }

  return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len,
                          &local_addr.su.sa, local_addr.len, buf.data(), nwrite,
                          0);
}

int QUICConnectionHandler::send_connection_close(
    const UpstreamAddr *faddr, uint32_t version, const ngtcp2_cid *ini_dcid,
    const ngtcp2_cid *ini_scid, const Address &remote_addr,
    const Address &local_addr, uint64_t error_code) {
  std::array<uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> buf;

  auto nwrite = ngtcp2_crypto_write_connection_close(
      buf.data(), buf.size(), version, ini_scid, ini_dcid, error_code);
  if (nwrite < 0) {
    LOG(ERROR) << "ngtcp2_crypto_write_connection_close failed";
    return -1;
  }

  if (LOG_ENABLED(INFO)) {
    LOG(INFO) << "Send Initial CONNECTION_CLOSE with error_code=" << log::hex
              << error_code << log::dec
              << " to remote=" << util::to_numeric_addr(&remote_addr)
              << " dcid=" << util::format_hex(ini_scid->data, ini_scid->datalen)
              << " scid="
              << util::format_hex(ini_dcid->data, ini_dcid->datalen);
  }

  return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len,
                          &local_addr.su.sa, local_addr.len, buf.data(), nwrite,
                          0);
}

void QUICConnectionHandler::add_connection_id(const ngtcp2_cid *cid,
                                              ClientHandler *handler) {
  connections_.emplace(*cid, handler);
}

void QUICConnectionHandler::remove_connection_id(const ngtcp2_cid *cid) {
  connections_.erase(*cid);
}

void QUICConnectionHandler::add_close_wait(CloseWait *cw) {
  for (auto &cid : cw->scids) {
    close_waits_.emplace(cid, cw);
  }
}

void QUICConnectionHandler::remove_close_wait(const CloseWait *cw) {
  for (auto &cid : cw->scids) {
    close_waits_.erase(cid);
  }
}

void QUICConnectionHandler::on_stateless_reset_bucket_regen() {
  assert(stateless_reset_bucket_ < SHRPX_QUIC_STATELESS_RESET_BURST);

  if (++stateless_reset_bucket_ == SHRPX_QUIC_STATELESS_RESET_BURST) {
    ev_timer_stop(worker_->get_loop(), &stateless_reset_bucket_regen_timer_);
  }
}

static void close_wait_timeoutcb(struct ev_loop *loop, ev_timer *w,
                                 int revents) {
  auto cw = static_cast<CloseWait *>(w->data);

  if (LOG_ENABLED(INFO)) {
    LOG(INFO) << "close-wait period finished";
  }

  auto quic_conn_handler = cw->worker->get_quic_connection_handler();
  quic_conn_handler->remove_close_wait(cw);

  delete cw;
}

CloseWait::CloseWait(Worker *worker, std::vector<ngtcp2_cid> scids,
                     std::vector<uint8_t> conn_close, ev_tstamp period)
    : worker{worker},
      scids{std::move(scids)},
      conn_close{std::move(conn_close)},
      bytes_recv{0},
      bytes_sent{0},
      num_pkts_recv{0},
      next_pkts_recv{1} {
  ++worker->get_worker_stat()->num_close_waits;

  ev_timer_init(&timer, close_wait_timeoutcb, period, 0.);
  timer.data = this;

  ev_timer_start(worker->get_loop(), &timer);
}

CloseWait::~CloseWait() {
  auto loop = worker->get_loop();

  ev_timer_stop(loop, &timer);

  auto worker_stat = worker->get_worker_stat();
  --worker_stat->num_close_waits;

  if (worker->get_graceful_shutdown() && worker_stat->num_connections == 0 &&
      worker_stat->num_close_waits == 0) {
    ev_break(loop);
  }
}

int CloseWait::handle_packet(const UpstreamAddr *faddr,
                             const Address &remote_addr,
                             const Address &local_addr, const uint8_t *data,
                             size_t datalen) {
  if (conn_close.empty()) {
    return 0;
  }

  ++num_pkts_recv;
  bytes_recv += datalen;

  if (bytes_sent + conn_close.size() > 3 * bytes_recv ||
      next_pkts_recv > num_pkts_recv) {
    return 0;
  }

  if (quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len,
                       &local_addr.su.sa, local_addr.len, conn_close.data(),
                       conn_close.size(), 0) != 0) {
    return -1;
  }

  next_pkts_recv *= 2;
  bytes_sent += conn_close.size();

  return 0;
}

} // namespace shrpx
