require "socket"

# This class wraps another `::Socket::Server` in an SSL layer.
#
# ```
# require "socket"
# require "openssl"
#
# tcp_server = TCPServer.new(0)
#
# ssl_context = OpenSSL::SSL::Context::Server.new
# ssl_context.certificate_chain = "openssl.crt"
# ssl_context.private_key = "openssl.key"
# ssl_server = OpenSSL::SSL::Server.new(tcp_server, ssl_context)
#
# puts "SSL Server listening on #{tcp_server.local_address}"
# while connection = ssl_server.accept?
#   connection.puts "secure message"
#   connection.close
# end
# ```
class OpenSSL::SSL::Server
  include ::Socket::Server

  # Returns the wrapped server socket.
  getter wrapped : ::Socket::Server

  # Returns the SSL context.
  getter context : OpenSSL::SSL::Context::Server

  # If `#sync_close?` is `true`, closing this server will
  # close the wrapped server.
  property? sync_close : Bool

  # Returns `true` if this SSL server has been closed.
  getter? closed : Bool = false

  # When `true` a call to `#accept` will also initiate the SSL handshake.
  property start_immediately : Bool = true

  # Creates a new SSL server wrapping *wrapped*.
  #
  # *context* configures the SSL options, see `OpenSSL::SSL::Context::Server` for details
  def initialize(@wrapped : ::Socket::Server, @context : OpenSSL::SSL::Context::Server = OpenSSL::SSL::Context::Server.new, @sync_close : Bool = true)
  end

  # Creates a new SSL server wrapping *wrapped*  and yields it to the block.
  #
  # *context* configures the SSL options, see `OpenSSL::SSL::Context::Server` for details
  #
  # The server is closed after the block returns.
  def self.open(wrapped : ::Socket::Server, context : OpenSSL::SSL::Context::Server = OpenSSL::SSL::Context::Server.new, sync_close : Bool = true)
    server = new(wrapped, context, sync_close)

    begin
      yield server
    ensure
      server.close
    end
  end

  # Implements `::Socket::Server#accept`.
  #
  # This method calls `@wrapped.accept` and wraps the resulting IO in a SSL socket (`OpenSSL::SSL::Socket::Server`) with `context` configuration.
  def accept : OpenSSL::SSL::Socket::Server
    new_ssl_socket(@wrapped.accept)
  end

  # Implements `::Socket::Server#accept?`.
  #
  # This method calls `@wrapped.accept?` and wraps the resulting IO in a SSL socket (`OpenSSL::SSL::Socket::Server`) with `context` configuration.
  def accept? : OpenSSL::SSL::Socket::Server?
    if socket = @wrapped.accept?
      new_ssl_socket(socket)
    end
  end

  private def new_ssl_socket(io)
    OpenSSL::SSL::Socket::Server.new(io, @context, sync_close: @sync_close, accept: @start_immediately)
  end

  # Closes this SSL server.
  #
  # Propagates to `wrapped` if `sync_close` is `true`.
  def close : Nil
    return if @closed
    @closed = true

    @wrapped.close if @sync_close
  end

  # Returns local address of `wrapped`.
  def local_address : ::Socket::Address
    @wrapped.local_address
  end
end
