# Ported from OpenSSL include/internal/ktls.h private header to enable KTLS
# through a custom BIO so the actual TCP communication goes through the normal
# Crystal::EventLoop regardless of the socket's blocking mode.
#
# Copyright 2018-2024 The OpenSSL Project Authors. All Rights Reserved.
#
# Licensed under the Apache License 2.0 (the "License").  You may not use
# this file except in compliance with the License.  You can obtain a copy
# in the file LICENSE in the source distribution or at
# https://www.openssl.org/source/license.html

# libressl doesn't support Kernel TLS
{% skip_file if compare_versions(LibSSL::LIBRESSL_VERSION, "0.0.0") > 0 %}

# libevent evloop doesn't implement recvmsg/sendmsg
{% skip_file if Crystal::EventLoop.has_constant?(:LibEvent) %}

{% if flag?(:linux) %}
  require "c/linux/tls"
{% elsif flag?(:freebsd) %}
  require "c/sys/ktls"
{% else %}
  {% skip_file %}
{% end %}

require "c/netinet/tcp"
require "c/sys/socket"

module OpenSSL
  # :nodoc:
  module KTLS
    private CMSG_LEVEL =
      {% if flag?(:linux) %}
        LibC::SOL_TLS
      {% elsif flag?(:freebsd) %}
        LibC::IPPROTO_TCP
      {% end %}

    # When successful, this socket option doesn't change the behaviour of the
    # TCP socket, except changing the TCP setsockopt handler to enable the
    # processing of SOL_TLS socket options. All other functionality remains the
    # same.
    #
    # Socket must be in TCP established state to enable KTLS. Further calls to
    # enable ktls will return EEXIST
    def self.enable(socket : Socket) : Bool
      {% if flag?(:linux) %}
        LibC.setsockopt(socket.fd, LibC::SOL_TCP, LibC::TCP_ULP, "tls", "tls".bytesize + 1) == 0
      {% elsif flag?(:freebsd) %}
        true
      {% end %}
    end

    def self.enable_tx_zerocopy_sendfile(socket : Socket) : Bool
      {% if flag?(:linux) %}
        enable = LibC::Int.new(1)
        LibC.setsockopt(socket.fd, LibC::SOL_TLS, LibC::TLS_TX_ZEROCOPY_RO, pointerof(enable), sizeof(LibC::Int)) == 0
      {% elsif flag?(:freebsd) %}
        true
      {% end %}
    end

    # The TLS_TX socket option changes the send/sendmsg handlers of the TCP
    # socket. If successful, then data sent using this socket will be encrypted
    # and encapsulated in TLS records using the crypto_info provided here.
    #
    # The TLS_RX socket option changes the recv/recvmsg handlers of the TCP
    # socket. If successful, then data received using this socket will be
    # decrypted, authenticated and decapsulated using the crypto_info provided
    # here.
    def self.start(socket : Socket, crypto_info : Pointer, is_tx : Bool) : Bool
      {% if flag?(:linux) %}
        if len = crypto_info_len(crypto_info)
          optname = is_tx ? LibC::TLS_TX : LibC::TLS_RX
          LibC.setsockopt(socket.fd, LibC::SOL_TLS, optname, crypto_info, len) == 0
        else
          return false
        end
      {% elsif flag?(:freebsd) %}
        optname = is_tx ? LibC::TCP_TXTLS_ENABLE : LibC::TCP_RXTLS_ENABLE
        LibC.setsockopt(socket.fd, LibC::IPPROTO_TCP, optname, crypto_info, sizeof(LibC::Tls_enable)) == 0
      {% end %}
    end

    {% if flag?(:linux) %}
      # OpenSSL uses an internal struct { union { cipher... }, len } and of
      # course the union size depends on how and where openssl has been built,
      # for example against which linux kernel headers.
      #
      # Hopefully 'struct crypto_info' puts the cipher version/type first, so
      # we can at least handle an allow list for currently supported ciphers.
      private def self.crypto_info_len(crypto_info)
        case type = crypto_info.as(LibC::Tls_crypto_info*).value.cipher_type
        when LibC::TLS_CIPHER_AES_GCM_128
          sizeof(LibC::Tls12_crypto_info_aes_gcm_128)
        when LibC::TLS_CIPHER_AES_GCM_256
          sizeof(LibC::Tls12_crypto_info_aes_gcm_256)
        when LibC::TLS_CIPHER_AES_CCM_128
          sizeof(LibC::Tls12_crypto_info_aes_ccm_128)
        when LibC::TLS_CIPHER_CHACHA20_POLY1305
          sizeof(LibC::Tls12_crypto_info_chacha20_poly1305)
        when LibC::TLS_CIPHER_SM4_GCM
          sizeof(LibC::Tls12_crypto_info_sm4_gcm)
        when LibC::TLS_CIPHER_SM4_CCM
          sizeof(LibC::Tls12_crypto_info_sm4_ccm)
        when LibC::TLS_CIPHER_ARIA_GCM_128
          sizeof(LibC::Tls12_crypto_info_aria_gcm_128)
        when LibC::TLS_CIPHER_ARIA_GCM_256
          sizeof(LibC::Tls12_crypto_info_aria_gcm_256)
        else
          # unknown TLS cipher (Linux 6.17, OpenSSL 3.6), check linux/tls.h and
          # 'struct tls_crypto_info_all' in openssl/include/internal/ktls.h
          STDERR.print "WARNING: unknown TLS cipher (skipping Kernel TLS)\n"
          nil
        end
      end
    {% end %}

    # Send a TLS record using the crypto_info provided in ktls_start and use
    # record_type instead of the default SSL3_RT_APPLICATION_DATA.
    # When the socket is non-blocking, then this call either returns EAGAIN or
    # the entire record is pushed to TCP. It is impossible to send a partial
    # record using this control message.
    def self.send_ctrl_message(socket : Socket, record_type : UInt8, data : UInt8*, length : Int32) : Int32 | Errno
      buf = uninitialized UInt8[24] # CMSG_SPACE(sizeof(record_type))

      cmsg = buf.to_unsafe.as(LibC::Cmsghdr*)
      cmsg.value.cmsg_level = CMSG_LEVEL
      cmsg.value.cmsg_type = LibC::TLS_SET_RECORD_TYPE
      cmsg.value.cmsg_len = sizeof(LibC::Cmsghdr) + sizeof(UInt8) # CMSG_LEN(sizeof(record_type))
      cmsg.value.cmsg_data.to_unsafe.value = record_type

      msg_iov = LibC::Iovec.new
      msg_iov.iov_base = data.as(Void*)
      msg_iov.iov_len = length

      msg = LibC::Msghdr.new
      msg.msg_control = buf.to_unsafe.as(Void*)
      msg.msg_controllen = cmsg.value.cmsg_len
      msg.msg_iov = pointerof(msg_iov)
      msg.msg_iovlen = 1

      Crystal::EventLoop.current.sendmsg(socket, pointerof(msg), 0)
    end

    # Receive a TLS record using the crypto_info provided in ktls_start.
    # The kernel strips the TLS record header, IV and authentication tag,
    # returning only the plaintext data or an error on failure.
    # We add the TLS record header here to satisfy routines in rec_layer_s3.c
    def self.read_record(socket : Socket, data : UInt8*, length : Int32) : Int32 | Errno
      buf = uninitialized UInt8[24] # CMSG_SPACE(sizeof(record_type))
      p = data.as(UInt8*)
      prepend_length = LibCrypto::SSL3_RT_HEADER_LENGTH

      if data_too_small?(length, prepend_length)
        return Errno::EINVAL
      end

      cdata_len =
        {% if flag?(:linux) %}
          sizeof(LibC::Char)
        {% elsif flag?(:freebsd) %}
          sizeof(LibC::Tls_get_record)
        {% end %}

      msg_iov = LibC::Iovec.new
      msg_iov.iov_base = (p + LibCrypto::SSL3_RT_HEADER_LENGTH).as(Void*)
      msg_iov.iov_len = iov_len(length - prepend_length)

      msg = LibC::Msghdr.new
      msg.msg_control = buf.to_unsafe.as(Void*)
      msg.msg_controllen = buf.size
      msg.msg_iov = pointerof(msg_iov)
      msg.msg_iovlen = 1

      ret = Crystal::EventLoop.current.recvmsg(socket, pointerof(msg), 0)
      return ret if ret.is_a?(Errno)

      {% if flag?(:linux) %}
        if msg.msg_controllen > 0
          cmsg = msg.msg_control.as(LibC::Cmsghdr*)

          if cmsg.value.cmsg_type == LibC::TLS_GET_RECORD_TYPE
            p[0] = cmsg.value.cmsg_data.to_unsafe.value # tls record type
            p[1] = LibCrypto::TLS1_2_VERSION_MAJOR.to_u8!
            p[2] = LibCrypto::TLS1_2_VERSION_MINOR.to_u8!
            # returned length is limited to msg_iov.iov_len above
            p[3] = (ret >> 8).to_u8!
            p[4] = ret.to_u8!
            ret += prepend_length
          end
        end
      {% elsif flag?(:freebsd) %}
        if (msg.msg_flags & (LibC::MSG_EOR | LibC::MSG_CTRUNC)) != LibC::MSG_EOR
          return Errno::EMSGSIZE
        end

        if msg.msg_controllen == 0
          return Errno::EBADMSG
        end

        cmsg = msg.msg_control.as(LibC::Cmsghdr*)

        if cmsg.value.cmsg_level != LibC::IPPROTO_TCP ||
           cmsg.value.cmsg_type != LibC::TLS_GET_RECORD ||
           cmsg.value.cmsg_len != sizeof(LibC::Cmsghdr) + cdata_len # CMSGLEN(cdata_len)
          return Errno::EBADMSG
        end

        tgr = cmsg.value.cmsg_data.to_unsafe.as(LibC::Tls_get_record*)
        p[0] = tgr.value.tls_type
        p[1] = tgr.value.tls_vmajor
        p[2] = tgr.value.tls_vminor
        p[3] = (ret >> 8).to_u8!
        p[4] = ret.to_u8!

        ret += prepend_length
      {% end %}

      ret
    end

    private def self.data_too_small?(length, prepend_length)
      {% if flag?(:linux) %}
        length < prepend_length + LibCrypto::EVP_GCM_TLS_TAG_LEN
      {% elsif flag?(:freebsd) %}
        length <= prepend_length
      {% end %}
    end

    private def self.iov_len(length)
      {% if flag?(:linux) %}
        length - LibCrypto::EVP_GCM_TLS_TAG_LEN
      {% elsif flag?(:freebsd) %}
        length
      {% end %}
    end
  end
end
