lib/gsmlg/socket.ex

defmodule GSMLG.Socket do
  @type t :: GSMLG.Socket.Protocol.t()

  @default_port_ws 80
  @default_port_wss 443

  defmodule Error do
    defexception message: nil

    def exception(reason: reason) do
      message =
        cond do
          msg = GSMLG.Socket.TCP.error(reason) ->
            msg

          msg = GSMLG.Socket.SSL.error(reason) ->
            msg

          true ->
            reason |> to_string
        end

      %Error{message: message}
    end
  end

  @doc ~S"""
  Create a socket connecting to somewhere using an URI.
  ## Supported URIs
  * `tcp://host:port` for GSMLG.Socket.TCP
  * `ssl://host:port` for GSMLG.Socket.SSL
  * `ws://host:port/path` for GSMLG.Socket.Web (using GSMLG.Socket.TCP)
  * `wss://host:port/path` for GSMLG.Socket.Web (using GSMLG.Socket.SSL)
  * `udp://host:port` for GSMLG.Socket:UDP
  ## Example
      { :ok, client } = GSMLG.Socket.connect "tcp://google.com:80"
      client.send "GET / HTTP/1.1\r\n"
      client.recv
  """
  @spec connect(String.t() | URI.t()) :: {:ok, GSMLG.Socket.t()} | {:error, any}
  def connect(uri) when uri |> is_list or uri |> is_binary do
    connect(URI.parse(uri))
  end

  def connect(%URI{scheme: "tcp", host: host, port: port}) do
    GSMLG.Socket.TCP.connect(host, port)
  end

  def connect(%URI{scheme: "ssl", host: host, port: port}) do
    GSMLG.Socket.SSL.connect(host, port)
  end

  def connect(%URI{scheme: "ws", host: host, port: port, path: path}) do
    GSMLG.Socket.Web.connect(host, port || @default_port_ws, path: path)
  end

  def connect(%URI{scheme: "wss", host: host, port: port, path: path}) do
    GSMLG.Socket.Web.connect(host, port || @default_port_wss, path: path, secure: true)
  end

  @doc """
  Create a socket connecting to somewhere using an URI, raising if an error
  occurs, see `connect`.
  """
  @spec connect!(String.t() | URI.t()) :: GSMLG.Socket.t() | no_return
  def connect!(uri) when uri |> is_list or uri |> is_binary do
    connect!(URI.parse(uri))
  end

  def connect!(%URI{scheme: "tcp", host: host, port: port}) do
    GSMLG.Socket.TCP.connect!(host, port)
  end

  def connect!(%URI{scheme: "ssl", host: host, port: port}) do
    GSMLG.Socket.SSL.connect!(host, port)
  end

  def connect!(%URI{scheme: "ws", host: host, port: port, path: path}) do
    GSMLG.Socket.Web.connect!(host, port || @default_port_ws, path: path)
  end

  def connect!(%URI{scheme: "wss", host: host, port: port, path: path}) do
    GSMLG.Socket.Web.connect!(host, port || @default_port_wss, path: path, secure: true)
  end

  @doc """
  Create a socket listening somewhere using an URI.
  ## Supported URIs
  If host is `*` it will be converted to `0.0.0.0`.
  * `tcp://host:port` for GSMLG.Socket.TCP
  * `ssl://host:port` for GSMLG.Socket.SSL
  * `ws://host:port/path` for GSMLG.Socket.Web (using GSMLG.Socket.TCP)
  * `wss://host:port/path` for GSMLG.Socket.Web (using GSMLG.Socket.SSL)
  * `udp://host:port` for GSMLG.Socket:UDP
  ## Example
      { :ok, server } = GSMLG.Socket.listen "tcp://*:1337"
      client = server |> GSMLG.Socket.accept!(packet: :line)
      client |> GSMLG.Socket.Stream.send(client.recv)
      client |> GSMLG.Socket.Stream.close
  """
  @spec listen(String.t() | URI.t()) :: {:ok, GSMLG.Socket.t()} | {:error, any}
  def listen(uri) when uri |> is_list or uri |> is_binary do
    listen(URI.parse(uri))
  end

  def listen(%URI{scheme: "tcp", host: host, port: port}) do
    GSMLG.Socket.TCP.listen(port, local: [address: if(host == "*", do: "0.0.0.0", else: host)])
  end

  def listen(%URI{scheme: "ssl", host: host, port: port}) do
    GSMLG.Socket.SSL.listen(port, local: [address: if(host == "*", do: "0.0.0.0", else: host)])
  end

  def listen(%URI{scheme: "ws", host: host, port: port}) do
    GSMLG.Socket.Web.listen(port || @default_port_ws,
      local: [address: if(host == "*", do: "0.0.0.0", else: host)]
    )
  end

  def listen(%URI{scheme: "wss", host: host, port: port}) do
    GSMLG.Socket.Web.listen(port || @default_port_wss,
      secure: true,
      local: [address: if(host == "*", do: "0.0.0.0", else: host)]
    )
  end

  @doc """
  Create a socket listening somewhere using an URI, raising if an error occurs,
  see `listen`.
  """
  @spec listen!(String.t() | URI.t()) :: GSMLG.Socket.t() | no_return
  def listen!(uri) when uri |> is_list or uri |> is_binary do
    listen!(URI.parse(uri))
  end

  def listen!(%URI{scheme: "tcp", host: host, port: port}) do
    GSMLG.Socket.TCP.listen!(port, local: [address: if(host == "*", do: "0.0.0.0", else: host)])
  end

  def listen!(%URI{scheme: "ssl", host: host, port: port}) do
    GSMLG.Socket.SSL.listen!(port, local: [address: if(host == "*", do: "0.0.0.0", else: host)])
  end

  def listen!(%URI{scheme: "ws", host: host, port: port}) do
    GSMLG.Socket.Web.listen!(port || @default_port_ws,
      local: [address: if(host == "*", do: "0.0.0.0", else: host)]
    )
  end

  def listen!(%URI{scheme: "wss", host: host, port: port}) do
    GSMLG.Socket.Web.listen!(port || @default_port_wss,
      secure: true,
      local: [address: if(host == "*", do: "0.0.0.0", else: host)]
    )
  end

  @doc false
  def arguments(options) do
    options =
      options
      |> Keyword.put_new(:mode, :passive)

    Enum.flat_map(options, fn
      {:mode, :active} ->
        [{:active, true}]

      {:mode, :once} ->
        [{:active, :once}]

      {:mode, :passive} ->
        [{:active, false}]

      {:route, true} ->
        [{:dontroute, false}]

      {:route, false} ->
        [{:dontroute, true}]

      {:reuse, true} ->
        [{:reuseaddr, true}]

      {:reuse, false} ->
        []

      {:linger, value} ->
        [{:linger, {true, value}}]

      {:priority, value} ->
        [{:priority, value}]

      {:tos, value} ->
        [{:tos, value}]

      {:send, options} ->
        Enum.flat_map(options, fn
          {:timeout, {timeout, :close}} ->
            [{:send_timeout, timeout}, {:send_timeout_close, true}]

          {:timeout, timeout} when timeout |> is_integer ->
            [{:send_timeout, timeout}]

          {:delay, delay} ->
            [{:delay_send, delay}]

          {:buffer, buffer} ->
            [{:sndbuf, buffer}]
        end)

      {:recv, options} ->
        Enum.flat_map(options, fn
          {:buffer, buffer} ->
            [{:recbuf, buffer}]
        end)
    end)
  end

  use GSMLG.Socket.Helpers

  defdelegate equal?(self, other), to: GSMLG.Socket.Protocol

  defdelegate accept(self), to: GSMLG.Socket.Protocol
  defbang(accept(self), to: GSMLG.Socket.Protocol)

  defdelegate accept(self, options), to: GSMLG.Socket.Protocol
  defbang(accept(self, options), to: GSMLG.Socket.Protocol)

  defdelegate options(self, opts), to: GSMLG.Socket.Protocol
  defbang(options(self, opts), to: GSMLG.Socket.Protocol)

  defdelegate packet(self, type), to: GSMLG.Socket.Protocol
  defbang(packet(self, type), to: GSMLG.Socket.Protocol)

  defdelegate process(self, pid), to: GSMLG.Socket.Protocol
  defbang(process(self, pid), to: GSMLG.Socket.Protocol)

  defdelegate active(self), to: GSMLG.Socket.Protocol
  defbang(active(self), to: GSMLG.Socket.Protocol)

  defdelegate active(self, mode), to: GSMLG.Socket.Protocol
  defbang(active(self, mode), to: GSMLG.Socket.Protocol)

  defdelegate passive(self), to: GSMLG.Socket.Protocol
  defbang(passive(self), to: GSMLG.Socket.Protocol)

  defdelegate local(self), to: GSMLG.Socket.Protocol
  defbang(local(self), to: GSMLG.Socket.Protocol)

  defdelegate remote(self), to: GSMLG.Socket.Protocol
  defbang(remote(self), to: GSMLG.Socket.Protocol)

  defdelegate close(self), to: GSMLG.Socket.Protocol
  defbang(close(self), to: GSMLG.Socket.Protocol)
end

defprotocol GSMLG.Socket.Protocol do
  @doc """
  Check the two sockets are the same.
  """
  @spec equal?(t, t) :: boolean
  def equal?(self, other)

  @doc """
  Accept a connection from the socket.
  """
  @spec accept(t) :: {:ok, t} | {:error, term}
  @spec accept(t, Keyword.t()) :: {:ok, t} | {:error, term}
  def accept(self, options \\ [])

  @doc """
  Set options for the socket.
  """
  @spec options(t, Keyword.t()) :: :ok | {:error, term}
  def options(self, opts)

  @doc """
  Change the packet type of the socket.
  """
  @spec packet(t, atom) :: :ok | {:error, term}
  def packet(self, type)

  @doc """
  Change the controlling process of the socket.
  """
  @spec process(t, pid) :: :ok | {:error, term}
  def process(self, pid)

  @doc """
  Make the socket active.
  """
  @spec active(t) :: :ok | {:error, term}
  def active(self)

  @doc """
  Make the socket active once.
  """
  @spec active(t, :once) :: :ok | {:error, term}
  def active(self, mode)

  @doc """
  Make the socket passive.
  """
  @spec passive(t) :: :ok | {:error, term}
  def passive(self)

  @doc """
  Get the local address/port of the socket.
  """
  @spec local(t) :: {:ok, {Socket.Address.t(), :inet.port_number()}} | {:error, term}
  def local(self)

  @doc """
  Get the remote address/port of the socket.
  """
  @spec remote(t) :: {:ok, {Socket.Address.t(), :inet.port_number()}} | {:error, term}
  def remote(self)

  @doc """
  Close the socket.
  """
  @spec close(t) :: :ok | {:error, term}
  def close(self)
end

defimpl GSMLG.Socket.Protocol, for: Port do
  def equal?(self, other) when other |> is_port do
    self == other
  end

  def equal?(self, other) do
    self == elem(other, 1)
  end

  def accept(self, options \\ []) do
    case :inet_db.lookup_socket(self) do
      {:ok, mod} when mod in [:inet_tcp, :inet6_tcp] ->
        GSMLG.Socket.TCP.accept(self, options)

      {:ok, mod} when mod in [:inet_udp, :inet6_udp] ->
        {:error, :einval}
    end
  end

  def options(self, opts) do
    :inet.setopts(self, GSMLG.Socket.arguments(opts))
  end

  def packet(self, type) do
    :inet.setopts(self, packet: type)
  end

  def process(self, pid) do
    case :inet_db.lookup_socket(self) do
      {:ok, mod} when mod in [:inet_tcp, :inet6_tcp] ->
        :gen_tcp.controlling_process(self, pid)

      {:ok, mod} when mod in [:inet_udp, :inet6_udp] ->
        :gen_udp.controlling_process(self, pid)
    end
  end

  def active(self) do
    :inet.setopts(self, active: true)
  end

  def active(self, :once) do
    :inet.setopts(self, active: :once)
  end

  def passive(self) do
    :inet.setopts(self, active: false)
  end

  def local(self) do
    :inet.sockname(self)
  end

  def remote(self) do
    :inet.peername(self)
  end

  def close(self) do
    :inet.close(self)
  end
end

defimpl GSMLG.Socket.Protocol, for: Tuple do
  require Record

  def equal?(self, other)
      when self |> Record.is_record(:sslsocket) and other |> Record.is_record(:sslsocket) do
    self == other
  end

  def equal?(self, other) when self |> Record.is_record(:sslsocket) do
    self |> elem(1) == other
  end

  def equal?(_, _) do
    false
  end

  def accept(self, options \\ []) when self |> Record.is_record(:sslsocket) do
    GSMLG.Socket.SSL.accept(self, options)
  end

  def options(self, opts) when self |> Record.is_record(:sslsocket) do
    GSMLG.Socket.SSL.options(self, opts)
  end

  def packet(self, type) when self |> Record.is_record(:sslsocket) do
    :ssl.setopts(self, packet: type)
  end

  def process(self, pid) when self |> Record.is_record(:sslsocket) do
    :ssl.controlling_process(self, pid)
  end

  def active(self) when self |> Record.is_record(:sslsocket) do
    :ssl.setopts(self, active: true)
  end

  def active(self, :once) when self |> Record.is_record(:sslsocket) do
    :ssl.setopts(self, active: :once)
  end

  def passive(self) when self |> Record.is_record(:sslsocket) do
    :ssl.setopts(self, active: false)
  end

  def local(self) when self |> Record.is_record(:sslsocket) do
    :ssl.sockname(self)
  end

  def remote(self) when self |> Record.is_record(:sslsocket) do
    :ssl.peername(self)
  end

  def close(self) when self |> Record.is_record(:sslsocket) do
    :ssl.close(self)
  end
end