defmodule ThousandIsland.Transports.SSL do
@moduledoc """
Defines a `ThousandIsland.Transport` implementation based on TCP SSL sockets
as provided by Erlang's `:ssl` module. For the most part, users of Thousand
Island will only ever need to deal with this module via `transport_options`
passed to `ThousandIsland` at startup time. A complete list of such options
is defined via the `t::ssl.tls_server_option` type. This list can be somewhat
difficult to decipher; a list of the most common options follows:
* `key`: A DER encoded binary representation of the SSL key to use
* `cert`: A DER encoded binary representation of the SSL key to use
* `keyfile`: A string path to a PEM encoded key to use for SSL
* `certfile`: A string path to a PEM encoded cert to use for SSL
* `ip`: The IP to listen on (defaults to all interfaces). IPs should be
described in tuple form (ie: `ip: {1, 2, 3, 4}`). The value `:loopback` can
be used to only bind to localhost. On platforms which support it (macOS and
Linux at a minimum, likely others), you can also bind to a Unix domain socket
by specifying a value of `ip: {:local, "/path/to/socket"}`. Note that the port
*must* be set to `0`, and that the socket is not removed from the filesystem
after the server shuts down.
Unless overridden, this module uses the following default options:
```elixir
backlog: 1024,
nodelay: true,
linger: {true, 30},
send_timeout: 30_000,
send_timeout_close: true,
reuseaddr: true
```
The following options are required for the proper operation of Thousand Island
and cannot be overridden at startup (though they can be set via calls to `setopts/2`)
```elixir
mode: :binary,
active: false
```
"""
@type options() :: [:ssl.tls_server_option()]
@behaviour ThousandIsland.Transport
@hardcoded_options [mode: :binary, active: false]
@impl ThousandIsland.Transport
def listen(port, user_options) do
default_options = [
backlog: 1024,
nodelay: true,
linger: {true, 30},
send_timeout: 30_000,
send_timeout_close: true,
reuseaddr: true
]
resolved_options = @hardcoded_options ++ user_options ++ default_options
if not Enum.any?([:keyfile, :key], &:proplists.is_defined(&1, resolved_options)) do
raise "transport_options must include one of keyfile or key"
end
if not Enum.any?([:certfile, :cert], &:proplists.is_defined(&1, resolved_options)) do
raise "transport_options must include one of certfile or cert"
end
:ssl.listen(port, resolved_options)
end
@impl ThousandIsland.Transport
defdelegate accept(listener_socket), to: :ssl, as: :transport_accept
@impl ThousandIsland.Transport
defdelegate handshake(socket), to: :ssl
@impl ThousandIsland.Transport
defdelegate controlling_process(socket, pid), to: :ssl
@impl ThousandIsland.Transport
defdelegate recv(socket, length, timeout), to: :ssl
@impl ThousandIsland.Transport
defdelegate send(socket, data), to: :ssl
@impl ThousandIsland.Transport
def sendfile(socket, filename, offset, length) do
# We can't use :file.sendfile here since it works on clear sockets, not ssl
# sockets. Build our own (much slower and not optimized for large files) version.
with {:ok, fd} <- :file.open(filename, [:raw]),
{:ok, data} <- :file.pread(fd, offset, length) do
case :ssl.send(socket, data) do
:ok -> {:ok, length}
{:error, error} -> {:error, error}
end
end
end
@impl ThousandIsland.Transport
defdelegate getopts(socket, options), to: :ssl
@impl ThousandIsland.Transport
defdelegate setopts(socket, options), to: :ssl
@impl ThousandIsland.Transport
defdelegate shutdown(socket, way), to: :ssl
@impl ThousandIsland.Transport
defdelegate close(socket), to: :ssl
@impl ThousandIsland.Transport
# :ssl's typespec is incorrect
@dialyzer {:no_match, local_info: 1}
def local_info(socket) do
case :ssl.sockname(socket) do
{:ok, {:local, path}} -> %{address: {:local, path}, port: 0, ssl_cert: nil}
{:ok, {ip, port}} -> %{address: ip, port: port, ssl_cert: nil}
other -> other
end
end
@impl ThousandIsland.Transport
def peer_info(socket) do
{:ok, {ip, port}} = :ssl.peername(socket)
cert =
case :ssl.peercert(socket) do
{:ok, cert} -> cert
{:error, _} -> nil
end
%{address: ip, port: port, ssl_cert: cert}
end
@impl ThousandIsland.Transport
def secure?, do: true
@impl ThousandIsland.Transport
defdelegate getstat(socket), to: :ssl
@impl ThousandIsland.Transport
defdelegate negotiated_protocol(socket), to: :ssl
end