defmodule Hell.Socket do @moduledoc """ Encapsulates a client connection's underlying socket, providing a facility to read, write, and otherwise manipulate a connection from a client. """ @enforce_keys [ :socket, :transport_module, :read_timeout, :handshake_timeout, :silent_terminate_on_error, :span ] defstruct @enforce_keys ++ [:read_timer, :read_deadline, :timer_deadline] @typedoc "A reference to a socket along with metadata describing how to use it" @type t :: %__MODULE__{ socket: Hell.Transport.socket(), transport_module: module(), read_timeout: timeout(), handshake_timeout: timeout(), read_timer: reference() | nil, read_deadline: integer() | nil, timer_deadline: integer() | nil, silent_terminate_on_error: boolean(), span: Hell.Telemetry.t() } @doc """ Creates a new socket struct based on the passed parameters. This is normally called internally by `Hell.Handler` and does not need to be called by implementations which are based on `Hell.Handler` """ @spec new( Hell.Transport.socket(), Hell.HandlerConfig.t(), Hell.Telemetry.t() ) :: t() def new(raw_socket, handler_config, span) do %__MODULE__{ socket: raw_socket, transport_module: handler_config.transport_module, read_timeout: handler_config.read_timeout, handshake_timeout: handler_config.handshake_timeout, silent_terminate_on_error: handler_config.silent_terminate_on_error, span: span } end @doc """ Handshakes the underlying socket if it is required (as in the case of SSL sockets, for example). This is normally called internally by `Hell.Handler` and does not need to be called by implementations which are based on `Hell.Handler` """ @spec handshake(t()) :: Hell.Transport.on_handshake() def handshake(%__MODULE__{} = socket) do case do_handshake(socket) do {:ok, inner_socket} -> {:ok, %{socket | socket: inner_socket}} {:error, _reason} = err -> # Note that we intentionally do not stop the connection span here. The # caller (Hell.Handler) surfaces this error and closes the # connection via `Hell.Handler.do_socket_close/2`, which is the # single place that stops the span. Stopping it here as well would emit a # duplicate `[:hell, :connection, :stop]` event. err end end defp do_handshake(%__MODULE__{} = socket) do if function_exported?(socket.transport_module, :handshake, 2) do socket.transport_module.handshake(socket.socket, socket.handshake_timeout) else socket.transport_module.handshake(socket.socket) end end @doc """ Upgrades the transport of the socket to use the specified transport module, performing any client handshaking that may be required. The passed options are blindly passed through to the new transport module. If the transport implements `c:Hell.Transport.upgrade/3`, `handshake_timeout` bounds its handshake; otherwise `c:Hell.Transport.upgrade/2` is used. This is normally called internally by `Hell.Handler` and does not need to be called by implementations which are based on `Hell.Handler` """ @spec upgrade(t(), module(), term()) :: Hell.Transport.on_upgrade() def upgrade(%__MODULE__{} = socket, module, opts) when is_atom(module) do result = if Code.ensure_loaded?(module) and function_exported?(module, :upgrade, 3) do module.upgrade(socket.socket, opts, socket.handshake_timeout) else module.upgrade(socket.socket, opts) end case result do {:ok, updated_socket} -> {:ok, %{socket | socket: updated_socket, transport_module: module}} {:error, _reason} = err -> # As with handshake/1, the span is stopped exactly once by the caller via # `Hell.Handler.do_socket_close/2`; stopping it here too would # emit a duplicate `[:hell, :connection, :stop]` event. err end end @doc """ Returns available bytes on the given socket. Up to `length` bytes will be returned (0 can be passed in to get the next 'available' bytes, typically the next packet). If insufficient bytes are available, the function can wait `timeout` milliseconds for data to arrive. """ @spec recv(t(), non_neg_integer(), timeout() | nil) :: Hell.Transport.on_recv() def recv(%__MODULE__{} = socket, length \\ 0, timeout \\ nil) do case socket.transport_module.recv(socket.socket, length, timeout || socket.read_timeout) do {:ok, data} = ok -> Hell.Telemetry.untimed_span_event(socket.span, :recv, %{data: data}) ok {:error, reason} = err -> Hell.Telemetry.span_event(socket.span, :recv_error, %{error: reason}) err end end @doc """ Sends the given data (specified as a binary or an IO list) on the given socket. """ @spec send(t(), iodata()) :: Hell.Transport.on_send() def send(%__MODULE__{} = socket, data) do case socket.transport_module.send(socket.socket, data) do :ok -> Hell.Telemetry.untimed_span_event(socket.span, :send, %{data: data}) :ok {:error, reason} = err -> Hell.Telemetry.span_event(socket.span, :send_error, %{data: data, error: reason}) err end end @doc """ Sends the contents of the given file based on the provided offset & length """ @spec sendfile(t(), String.t(), non_neg_integer(), non_neg_integer()) :: Hell.Transport.on_sendfile() def sendfile(%__MODULE__{} = socket, filename, offset, length) do case socket.transport_module.sendfile(socket.socket, filename, offset, length) do {:ok, bytes_written} = ok -> measurements = %{filename: filename, offset: offset, bytes_written: bytes_written} Hell.Telemetry.untimed_span_event(socket.span, :sendfile, measurements) ok {:error, reason} = err -> measurements = %{filename: filename, offset: offset, length: length, error: reason} Hell.Telemetry.span_event(socket.span, :sendfile_error, measurements) err end end @doc """ Shuts down the socket in the given direction. """ @spec shutdown(t(), Hell.Transport.way()) :: Hell.Transport.on_shutdown() def shutdown(%__MODULE__{} = socket, way) do Hell.Telemetry.span_event(socket.span, :socket_shutdown, %{way: way}) socket.transport_module.shutdown(socket.socket, way) end @doc """ Closes the given socket. Note that a socket is automatically closed when the handler process which owns it terminates """ @spec close(t()) :: Hell.Transport.on_close() def close(%__MODULE__{} = socket) do socket.transport_module.close(socket.socket) end @doc """ Gets the given flags on the socket Errors are usually from :inet.posix(), however, SSL module defines return type as any() """ @spec getopts(t(), Hell.Transport.socket_get_options()) :: Hell.Transport.on_getopts() def getopts(%__MODULE__{} = socket, options) do socket.transport_module.getopts(socket.socket, options) end @doc """ Sets the given flags on the socket Errors are usually from :inet.posix(), however, SSL module defines return type as any() """ @spec setopts(t(), Hell.Transport.socket_set_options()) :: Hell.Transport.on_setopts() def setopts(%__MODULE__{} = socket, options) do socket.transport_module.setopts(socket.socket, options) end @doc """ Returns information in the form of `t:Hell.Transport.socket_info()` about the local end of the socket. """ @spec sockname(t()) :: Hell.Transport.on_sockname() def sockname(%__MODULE__{} = socket) do socket.transport_module.sockname(socket.socket) end @doc """ Returns information in the form of `t:Hell.Transport.socket_info()` about the remote end of the socket. """ @spec peername(t()) :: Hell.Transport.on_peername() def peername(%__MODULE__{} = socket) do socket.transport_module.peername(socket.socket) end @doc """ Returns information in the form of `t:public_key.der_encoded()` about the peer certificate of the socket. """ @spec peercert(t()) :: Hell.Transport.on_peercert() def peercert(%__MODULE__{} = socket) do socket.transport_module.peercert(socket.socket) end @doc """ Returns whether or not this protocol is secure. """ @spec secure?(t()) :: boolean() def secure?(%__MODULE__{} = socket) do socket.transport_module.secure?() end @doc """ Returns statistics about the connection. """ @spec getstat(t()) :: Hell.Transport.socket_stats() def getstat(%__MODULE__{} = socket) do socket.transport_module.getstat(socket.socket) end @doc """ Returns information about the protocol negotiated during transport handshaking (if any). """ @spec negotiated_protocol(t()) :: Hell.Transport.on_negotiated_protocol() def negotiated_protocol(%__MODULE__{} = socket) do socket.transport_module.negotiated_protocol(socket.socket) end @doc """ Returns information about the SSL connection info, if transport is SSL. """ @spec connection_information(t()) :: Hell.Transport.on_connection_information() def connection_information(%__MODULE__{} = socket) do socket.transport_module.connection_information(socket.socket) end @doc """ Returns the telemetry span representing the lifetime of this socket """ @spec telemetry_span(t()) :: Hell.Telemetry.t() def telemetry_span(%__MODULE__{} = socket) do socket.span end end