Dtls context

use "files"

use @DTLS_method[Pointer[_SSLMethod]]()
  if "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl"

class val DTLSContext
  """
  A DTLS context is used to create DTLS sessions.

  DTLS is the datagram variant of TLS. It provides the same authentication and
  encryption guarantees over unreliable transports like UDP. A DTLS context uses
  `DTLS_method()` internally and accepts only DTLS version numbers.

  This type is separate from `SSLContext` because DTLS and TLS are not
  interchangeable: a TLS context cannot create a DTLS session and vice versa.
  Keeping them separate makes the wrong combination a compile error.
  """
  var _ctx: Pointer[_SSLContext] tag
  var _client_verify: Bool = true
  var _server_verify: Bool = false
  var _alpn_resolver: (ALPNProtocolResolver val | None) = None

  new create() =>
    """
    Create a DTLS context.
    """
    ifdef
      "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl"
    then
      _ctx = @SSL_CTX_new(@DTLS_method())

      try
        set_min_proto_version(DTLS1u2Version())?
        set_max_proto_version(SSLAutoVersion())?
      end
    else
      compile_error "You must select an SSL version to use."
    end

  fun _ssl_ctx(): Pointer[_SSLContext] tag =>
    _ctx

  fun val client(hostname: String = ""): DTLS iso^ ? =>
    """
    Create a client-side DTLS session. If a hostname is supplied and client
    verification is on, the server side certificate must be valid for that
    hostname. Raises an error if the context has been disposed.

    The session holds the context, so the context lives for as long as the
    session can handshake.
    """
    let verify = _client_verify
    recover DTLS._create(this, false, verify, hostname)? end

  fun val server(): DTLS iso^ ? =>
    """
    Create a server-side DTLS session. Raises an error if the context has been
    disposed.

    The session holds the context, so the context and the ALPN resolver it
    installed with OpenSSL live for as long as the session can handshake.
    """
    let verify = _server_verify
    recover DTLS._create(this, true, verify)? end

  fun ref set_cert(cert: FilePath, key: FilePath) ? =>
    """
    The cert file is a PEM certificate chain. The key file is a private key.
    Servers must set this. For clients, it is optional. Raises an error if the
    context has been disposed.
    """
    if _ctx.is_null() then error end

    if
      (cert.path.size() == 0)
        or (key.path.size() == 0)
        or (0 == @SSL_CTX_use_certificate_chain_file(
          _ctx, cert.path.cstring()))
        or (0 == @SSL_CTX_use_PrivateKey_file(
          _ctx, key.path.cstring(), I32(1)))
        or (0 == @SSL_CTX_check_private_key(_ctx))
    then
      error
    end

  fun ref set_authority(
    file: (FilePath | None),
    path: (FilePath | None) = None)
    ?
  =>
    """
    Use a PEM file and/or a directory of PEM files to specify certificate
    authorities. Clients must set this. For servers, it is optional. Use None
    to indicate no file or no path. Raises an error if these verify locations
    aren't valid, or if the context has been disposed.

    If both `file` and `path` are `None`, on Windows this method loads the
    system root certificates. On Posix it raises an error.
    """
    if _ctx.is_null() then error end

    if (file is None) and (path is None) then
      ifdef windows then
        _load_windows_root_certs()?
      else
        error
      end
    else
      let fs = try (file as FilePath).path else "" end
      let ps = try (path as FilePath).path else "" end

      let f = if fs.size() > 0 then fs.cstring() else Pointer[U8] end
      let p = if ps.size() > 0 then ps.cstring() else Pointer[U8] end

      if
        (f.is_null() and p.is_null())
          or (0 == @SSL_CTX_load_verify_locations(_ctx, f, p))
      then
        error
      end
    end

  fun ref _load_windows_root_certs() ? =>
    ifdef windows then
      let root_str = "ROOT"
      let h_store = @CertOpenSystemStoreA(Pointer[None], root_str.cstring())
      if h_store.is_null() then error end

      let x509_store = @X509_STORE_new()
      if x509_store.is_null() then
        @CertCloseStore(h_store, U32(0))
        error
      end

      var p_context =
        @CertEnumCertificatesInStore(
          h_store, NullablePointer[_CertContext].none())

      try
        while true do
          let cert_context = try p_context()? else break end
          let x509 =
            @d2i_X509(
              Pointer[Pointer[X509]],
              addressof cert_context.pb_cert_encoded,
              cert_context.cb_cert_encoded.ilong())
          if not x509.is_null() then
            let result = @X509_STORE_add_cert(x509_store, x509)
            @X509_free(x509)
            if result != 1 then error end
          end

          p_context = @CertEnumCertificatesInStore(h_store, p_context)
        end

        @SSL_CTX_set_cert_store(_ctx, x509_store)
      else
        if not p_context.is_none() then
          @CertFreeCertificateContext(p_context)
        end
        @X509_STORE_free(x509_store)
      then
        @CertCloseStore(h_store, U32(0))
      end
    end

  fun ref set_ciphers(ciphers: String) ? =>
    """
    Set the accepted ciphers. This replaces the existing list. Raises an error
    if the cipher list is invalid, or if the context has been disposed.
    """
    if _ctx.is_null() then error end

    if 0 == @SSL_CTX_set_cipher_list(_ctx, ciphers.cstring()) then
      error
    end

  fun ref set_client_verify(state: Bool) =>
    """
    Set to true to require verification. Defaults to true.

    A client session created with `state` false never reports `SSLAuthFail`.
    """
    _client_verify = state

  fun ref set_server_verify(state: Bool) =>
    """
    Set to true to require verification. Defaults to false.

    A server session created with `state` false never reports `SSLAuthFail`.
    It sends no certificate request, so it has no peer identity to reject.
    """
    _server_verify = state

  fun ref set_verify_depth(depth: U32) =>
    """
    Set the verify depth. Defaults to 6. Does nothing if the context has been
    disposed.

    A depth of 2^31 or more arrives at the SSL library as a negative depth.
    What each backend does with one is undocumented, so do not use a depth that
    large.
    """
    if not _ctx.is_null() then
      @SSL_CTX_set_verify_depth(_ctx, depth.i32())
    end

  fun ref set_min_proto_version(version: ULong) ? =>
    """
    Set minimum protocol version. Set to SSLAutoVersion, 0, to automatically
    manage lowest version.

    Raises an error if the context has been disposed or if the SSL library
    rejects the version.

    Supported versions: DTLS1Version, DTLS1u2Version
    """
    if _ctx.is_null() then error end

    let result =
      @SSL_CTX_ctrl(
        _ctx, _SSLCtrlSetMinProtoVersion(), version.ilong(), Pointer[None])
    if result == 0 then
      error
    end

  fun get_min_proto_version(): ILong =>
    """
    Get minimum protocol version. Returns SSLAutoVersion, 0,
    when automatically managing lowest version. A disposed context returns
    SSLAutoVersion.

    Supported versions: DTLS1Version, DTLS1u2Version
    """
    if _ctx.is_null() then return SSLAutoVersion().ilong() end

    @SSL_CTX_ctrl(_ctx, _SSLCtrlGetMinProtoVersion(), 0, Pointer[None])

  fun ref set_max_proto_version(version: ULong) ? =>
    """
    Set maximum protocol version. Set to SSLAutoVersion, 0, to automatically
    manage highest version.

    Raises an error if the context has been disposed or if the SSL library
    rejects the version.

    Supported versions: DTLS1Version, DTLS1u2Version
    """
    if _ctx.is_null() then error end

    let result =
      @SSL_CTX_ctrl(
        _ctx, _SSLCtrlSetMaxProtoVersion(), version.ilong(), Pointer[None])
    if result == 0 then
      error
    end

  fun get_max_proto_version(): ILong =>
    """
    Get maximum protocol version. Returns SSLAutoVersion, 0,
    when automatically managing highest version. A disposed context returns
    SSLAutoVersion.

    Supported versions: DTLS1Version, DTLS1u2Version
    """
    if _ctx.is_null() then return SSLAutoVersion().ilong() end

    @SSL_CTX_ctrl(_ctx, _SSLCtrlGetMaxProtoVersion(), 0, Pointer[None])

  fun ref alpn_set_resolver(resolver: ALPNProtocolResolver val): Bool =>
    """
    Use `resolver` to choose the protocol to be selected for incoming
    connections.

    OpenSSL holds a raw pointer to `resolver` that the Pony garbage collector
    cannot see. The context keeps `resolver` alive, and every session made from
    the context keeps the context alive, so `resolver` lives for as long as any
    session that can reach it. The resolver has to be set before any session is
    created, which the capabilities enforce: this method needs a mutable
    context, and `client` and `server` need one that has been made immutable.

    Returns true on success. Returns false if the context has been disposed.
    """
    if _ctx.is_null() then return false end

    ifdef
      "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl"
    then
      _alpn_resolver = resolver
      @SSL_CTX_set_alpn_select_cb(
        _ctx, addressof SSLContext._alpn_select_cb, resolver)
      return true
    else
      compile_error "You must select an SSL version to use."
    end

  fun ref alpn_set_client_protocols(protocols: Array[String] box): Bool =>
    """
    Advertise the protocol names in `protocols` when connecting to a server.
    Each name must be between 1 and 255 bytes.

    Returns true on success. Returns false if the context has been disposed,
    if `protocols` is empty or holds a name of an unusable size, or if OpenSSL
    would not take the list.
    """
    if _ctx.is_null() then return false end

    ifdef
      "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl"
    then
      try
        let proto_list = _ALPNProtocolList.from_array(protocols)?
        let result =
          @SSL_CTX_set_alpn_protos(
            _ctx, proto_list.cpointer(), proto_list.size().u32())
        return result == 0
      end
    else
      compile_error "You must select an SSL version to use."
    end

    false

  fun ref dispose() =>
    """
    Free the DTLS context. A disposed context cannot create a session, and no
    configuration of it can take effect.
    """
    if not _ctx.is_null() then
      @SSL_CTX_free(_ctx)
      _ctx = Pointer[_SSLContext]
    end

  fun _final() =>
    if not _ctx.is_null() then
      @SSL_CTX_free(_ctx)
    end