defmodule GSMLG.Socket.Web do
@moduledoc ~S"""
This module implements RFC 6455 WebGSMLG.Sockets.
## Client example
socket = GSMLG.Socket.Web.connect! "echo.websocket.org"
socket |> GSMLG.Socket.Web.send! { :text, "test" }
socket |> GSMLG.Socket.Web.recv! # => {:text, "test"}
## Server example
server = GSMLG.Socket.Web.listen! 80
client = server |> GSMLG.Socket.Web.accept!
# here you can verify if you want to accept the request or not, call
# `GSMLG.Socket.Web.close!` if you don't want to accept it, or else call
# `GSMLG.Socket.Web.accept!`
client |> GSMLG.Socket.Web.accept!
# echo the first message
client |> GSMLG.Socket.Web.send!(client |> GSMLG.Socket.Web.recv!)
"""
use Bitwise
import Kernel, except: [length: 1, send: 2]
alias __MODULE__, as: W
@type error :: GSMLG.Socket.TCP.error() | GSMLG.Socket.SSL.error()
@type packet ::
{:text, String.t()}
| {:binary, binary}
| {:fragmented, :text | :binary | :continuation | :end, binary}
| :close
| {:close, atom, binary}
| {:ping, binary}
| {:pong, binary}
@compile {:inline, opcode: 1, close_code: 1, key: 1, length: 1, forge: 2}
Enum.each([text: 0x1, binary: 0x2, close: 0x8, ping: 0x9, pong: 0xA], fn {name, code} ->
defp opcode(unquote(name)), do: unquote(code)
defp opcode(unquote(code)), do: unquote(name)
end)
Enum.each(
[
normal: 1000,
going_away: 1001,
protocol_error: 1002,
unsupported_data: 1003,
reserved: 1004,
no_status_received: 1005,
abnormal: 1006,
invalid_payload: 1007,
policy_violation: 1008,
message_too_big: 1009,
mandatory_extension: 1010,
internal_error: 1011,
handshake: 1015
],
fn {name, code} ->
defp close_code(unquote(name)), do: unquote(code)
defp close_code(unquote(code)), do: unquote(name)
end
)
defmacrop known?(n) do
quote do
unquote(n) in [0x1, 0x2, 0x8, 0x9, 0xA]
end
end
defmacrop data?(n) do
quote do
unquote(n) in 0x1..0x7 or unquote(n) in [:text, :binary]
end
end
defmacrop control?(n) do
quote do
unquote(n) in 0x8..0xF or unquote(n) in [:close, :ping, :pong]
end
end
defstruct [
:socket,
:version,
:path,
:origin,
:protocols,
:extensions,
:key,
:mask,
{:headers, %{}}
]
@type t :: %GSMLG.Socket.Web{
socket: term,
version: 13,
path: String.t(),
origin: String.t(),
protocols: [String.t()],
extensions: [String.t()],
key: String.t(),
mask: boolean
}
@spec headers(%{String.t() => String.t()}, GSMLG.Socket.t(), Keyword.t()) :: %{
String.t() => String.t()
}
defp headers(acc, socket, options) do
case socket |> GSMLG.Socket.Stream.recv!(options) do
{:http_header, _, name, _, value} when name |> is_atom ->
acc
|> Map.put(Atom.to_string(name) |> String.downcase(), value)
|> headers(socket, options)
{:http_header, _, name, _, value} when name |> is_binary ->
acc |> Map.put(String.downcase(name), value) |> headers(socket, options)
:http_eoh ->
acc
end
end
@spec key(String.t()) :: String.t()
defp key(value) do
:crypto.hash(:sha, value <> "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") |> :base64.encode()
end
@doc """
Connects to the given address or { address, port } tuple.
"""
@spec connect({GSMLG.Socket.Address.t(), :inet.port_number()}) :: {:ok, t} | {:error, error}
def connect({address, port}) do
connect(address, port, [])
end
def connect(address) do
connect(address, [])
end
@doc """
Connect to the given address or { address, port } tuple with the given
options or address and port.
"""
@spec connect(
{GSMLG.Socket.Address.t(), :inet.port_number()} | GSMLG.Socket.Address.t(),
Keyword.t() | :inet.port_number()
) :: {:ok, t} | {:error, error}
def connect({address, port}, options) do
connect(address, port, options)
end
def connect(address, options) when options |> is_list do
connect(address, if(options[:secure], do: 443, else: 80), options)
end
def connect(address, port) when port |> is_integer do
connect(address, port, [])
end
@doc """
Connect to the given address, port and options.
## Options
`:path` sets the path to give the server, `/` by default
`:origin` sets the Origin header, this is optional
`:handshake` is the key used for the handshake, this is optional
You can also pass TCP or SSL options, depending if you're using secure
websockets or not.
"""
@spec connect(GSMLG.Socket.Address.t(), :inet.port_number(), Keyword.t()) ::
{:ok, t} | {:error, error}
def connect(address, port, options) do
try do
{:ok, connect!(address, port, options)}
rescue
e in [MatchError] ->
case e.term do
{:http_response, _, http_code, http_message} -> {:error, {http_code, http_message}}
_ -> {:error, "malformed handshake"}
end
e in [RuntimeError] ->
{:error, e.message}
e in [GSMLG.Socket.Error] ->
{:error, e.message}
e in [GSMLG.Socket.TCP.Error, GSMLG.Socket.SSL.Error] ->
{:error, e.code}
end
end
@doc """
Connects to the given address or { address, port } tuple, raising if an error
occurs.
"""
@spec connect!({GSMLG.Socket.Address.t(), :inet.port_number()}) :: t | no_return
def connect!({address, port}) do
connect!(address, port, [])
end
def connect!(address) do
connect!(address, [])
end
@doc """
Connect to the given address or { address, port } tuple with the given
options or address and port, raising if an error occurs.
"""
@spec connect!(
{GSMLG.Socket.Address.t(), :inet.port_number()} | GSMLG.Socket.Address.t(),
Keyword.t() | :inet.port_number()
) :: t | no_return
def connect!({address, port}, options) do
connect!(address, port, options)
end
def connect!(address, options) when options |> is_list do
connect!(address, if(options[:secure], do: 443, else: 80), options)
end
def connect!(address, port) when port |> is_integer do
connect!(address, port, [])
end
@doc """
Connect to the given address, port and options, raising if an error occurs.
## Options
`:path` sets the path to give the server, `/` by default
`:origin` sets the Origin header, this is optional
`:handshake` is the key used for the handshake, this is optional
`:headers` are additional headers that will be sent
You can also pass TCP or SSL options, depending if you're using secure
websockets or not.
"""
@spec connect!(GSMLG.Socket.Address.t(), :inet.port_number(), Keyword.t()) :: t | no_return
def connect!(address, port, options) do
{local, global} = arguments(options)
mod =
if local[:secure] do
GSMLG.Socket.SSL
else
GSMLG.Socket.TCP
end
path = local[:path] || "/"
origin = local[:origin]
protocols = local[:protocol]
extensions = local[:extensions]
handshake = :base64.encode(local[:handshake] || "fork the dongles")
headers = Enum.map(local[:headers] || %{}, fn {k, v} -> ["#{k}: #{v}", "\r\n"] end)
client = mod.connect!(address, port, global)
client |> GSMLG.Socket.packet!(:raw)
client
|> GSMLG.Socket.Stream.send!([
"GET #{path} HTTP/1.1",
"\r\n",
headers,
"Host: #{address}:#{port}",
"\r\n",
if(origin, do: ["Origin: #{origin}", "\r\n"], else: []),
"Upgrade: websocket",
"\r\n",
"Connection: Upgrade",
"\r\n",
"Sec-WebGSMLG.Socket-Key: #{handshake}",
"\r\n",
if(protocols,
do: ["Sec-WebGSMLG.Socket-Protocol: #{Enum.join(protocols, ", ")}", "\r\n"],
else: []
),
if(extensions,
do: ["Sec-WebGSMLG.Socket-Extensions: #{Enum.join(extensions, ", ")}", "\r\n"],
else: []
),
"Sec-WebGSMLG.Socket-Version: 13",
"\r\n",
"\r\n"
])
client |> GSMLG.Socket.packet(:http_bin)
{:http_response, _, 101, _} = client |> GSMLG.Socket.Stream.recv!(global)
headers = headers(%{}, client, local)
if String.downcase(headers["upgrade"] || "") != "websocket" or
String.downcase(headers["connection"] || "") != "upgrade" do
client |> GSMLG.Socket.close()
raise RuntimeError, message: "malformed upgrade response"
end
if headers["sec-websocket-version"] && headers["sec-websocket-version"] != "13" do
client |> GSMLG.Socket.close()
raise RuntimeError, message: "unsupported version"
end
if !headers["sec-websocket-accept"] or headers["sec-websocket-accept"] != key(handshake) do
client |> GSMLG.Socket.close()
raise RuntimeError, message: "wrong key response"
end
client |> GSMLG.Socket.packet!(:raw)
%W{socket: client, version: 13, path: path, origin: origin, key: handshake, mask: true}
end
@doc """
Listens on the default port (80).
"""
@spec listen :: {:ok, t} | {:error, error}
def listen do
listen([])
end
@doc """
Listens on the given port or with the given options.
"""
@spec listen(:inet.port_number() | Keyword.t()) :: {:ok, t} | {:error, error}
def listen(port) when port |> is_integer do
listen(port, [])
end
def listen(options) do
if options[:secure] do
listen(443, options)
else
listen(80, options)
end
end
@doc """
Listens on the given port with the given options.
## Options
`:secure` when true it will use SSL sockets
You can also pass TCP or SSL options, depending if you're using secure
websockets or not.
"""
@spec listen(:inet.port_number(), Keyword.t()) :: {:ok, t} | {:error, error}
def listen(port, options) do
{local, global} = arguments(options)
mod =
if local[:secure] do
GSMLG.Socket.SSL
else
GSMLG.Socket.TCP
end
case mod.listen(port, global) do
{:ok, socket} ->
{:ok, %W{socket: socket}}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Listens on the default port (80), raising if an error occurs.
"""
@spec listen! :: t | no_return
def listen! do
listen!([])
end
@doc """
Listens on the given port or with the given options, raising if an error
occurs.
"""
@spec listen!(:inet.port_number() | Keyword.t()) :: t | no_return
def listen!(port) when port |> is_integer do
listen!(port, [])
end
def listen!(options) do
if options[:secure] do
listen!(443, options)
else
listen!(80, options)
end
end
@doc """
Listens on the given port with the given options, raising if an error occurs.
## Options
`:secure` when true it will use SSL sockets
You can also pass TCP or SSL options, depending if you're using secure
websockets or not.
"""
@spec listen!(:inet.port_number(), Keyword.t()) :: t | no_return
def listen!(port, options) do
{local, global} = arguments(options)
mod =
if local[:secure] do
GSMLG.Socket.SSL
else
GSMLG.Socket.TCP
end
%W{socket: mod.listen!(port, global)}
end
@doc """
If you're calling this on a listening socket, it accepts a new client
connection.
If you're calling this on a client socket, it finalizes the acception
handshake, this separation is done because then you can verify the client can
connect based on Origin header, path and other things.
"""
@spec accept(t, Keyword.t()) :: {:ok, t} | {:error, error}
def accept(self, options \\ []) do
try do
{:ok, accept!(self, options)}
rescue
MatchError ->
{:error, "malformed handshake"}
e in [RuntimeError] ->
{:error, e.message}
e in [GSMLG.Socket.Error] ->
{:error, e.code}
end
end
@doc """
If you're calling this on a listening socket, it accepts a new client
connection.
If you're calling this on a client socket, it finalizes the acception
handshake, this separation is done because then you can verify the client can
connect based on Origin header, path and other things.
In case of error, it raises.
"""
@spec accept!(t, Keyword.t()) :: t | no_return
def accept!(socket, options \\ [])
def accept!(%W{socket: socket, key: nil}, options) do
{local, global} = arguments(options)
client = socket |> GSMLG.Socket.accept!(global)
client |> GSMLG.Socket.packet!(:http_bin)
path =
case client |> GSMLG.Socket.Stream.recv!(global) do
{:http_request, :GET, {:abs_path, path}, _} ->
path
end
headers = headers(%{}, client, local)
if headers["upgrade"] != "websocket" and headers["connection"] != "Upgrade" do
client |> GSMLG.Socket.close()
raise RuntimeError, message: "malformed upgrade request"
end
unless headers["sec-websocket-key"] do
client |> GSMLG.Socket.close()
raise RuntimeError, message: "missing key"
end
protocols =
if p = headers["sec-websocket-protocol"] do
String.split(p, ~r/\s*,\s*/)
end
extensions =
if e = headers["sec-websocket-extensions"] do
String.split(e, ~r/\s*,\s*/)
end
client |> GSMLG.Socket.packet!(:raw)
%W{
socket: client,
origin: headers["origin"],
path: path,
version: 13,
key: headers["sec-websocket-key"],
protocols: protocols,
extensions: extensions,
headers: headers
}
end
def accept!(%W{socket: socket, key: key}, options) do
{local, _} = arguments(options)
extensions = local[:extensions]
protocol = local[:protocol]
socket |> GSMLG.Socket.packet!(:raw)
socket
|> GSMLG.Socket.Stream.send!([
"HTTP/1.1 101 Switching Protocols",
"\r\n",
"Upgrade: websocket",
"\r\n",
"Connection: Upgrade",
"\r\n",
"Sec-WebGSMLG.Socket-Accept: #{key(key)}",
"\r\n",
"Sec-WebGSMLG.Socket-Version: 13",
"\r\n",
if(extensions,
do: ["Sec-WebGSMLG.Socket-Extensions: ", Enum.join(extensions, ", "), "\r\n"],
else: []
),
if(protocol, do: ["Sec-WebGSMLG.Socket-Protocol: ", protocol, "\r\n"], else: []),
"\r\n"
])
socket
end
@doc """
Extract websocket specific options from the rest.
"""
@spec arguments(Keyword.t()) :: {Keyword.t(), Keyword.t()}
def arguments(options) do
options =
Enum.group_by(options, fn
{:secure, _} -> true
{:path, _} -> true
{:origin, _} -> true
{:protocol, _} -> true
{:extensions, _} -> true
{:handshake, _} -> true
{:headers, _} -> true
_ -> false
end)
{Map.get(options, true, []), Map.get(options, false, [])}
end
@doc """
Return the local address and port.
"""
@spec local(t) :: {:ok, {:inet.ip_address(), :inet.port_number()}} | {:error, error}
def local(%W{socket: socket}) do
socket |> GSMLG.Socket.local()
end
@doc """
Return the local address and port, raising if an error occurs.
"""
@spec local!(t) :: {:inet.ip_address(), :inet.port_number()} | no_return
def local!(%W{socket: socket}) do
socket |> GSMLG.Socket.local!()
end
@doc """
Return the remote address and port.
"""
@spec remote(t) :: {:ok, {:inet.ip_address(), :inet.port_number()}} | {:error, error}
def remote(%W{socket: socket}) do
socket |> GSMLG.Socket.remote()
end
@doc """
Return the remote address and port, raising if an error occurs.
"""
@spec remote!(t) :: {:inet.ip_address(), :inet.port_number()} | no_return
def remote!(%W{socket: socket}) do
socket |> GSMLG.Socket.remote!()
end
@spec mask(binary) :: {integer, binary}
defp mask(data) do
case :crypto.strong_rand_bytes(4) do
<<key::32>> ->
{key, unmask(key, data)}
end
end
@spec mask(integer, binary) :: {integer, binary}
defp mask(key, data) do
{key, unmask(key, data)}
end
@spec unmask(integer, binary) :: binary
defp unmask(key, data) do
unmask(key, data, <<>>)
end
# we have to XOR the key with the data iterating over the key when there's
# more data, this means we can optimize and do it 4 bytes at a time and then
# fallback to the smaller sizes
defp unmask(key, <<data::32, rest::binary>>, acc) do
unmask(key, rest, <<acc::binary, bxor(data, key)::32>>)
end
defp unmask(key, <<data::24>>, acc) do
<<key::24, _::8>> = <<key::32>>
unmask(key, <<>>, <<acc::binary, bxor(data, key)::24>>)
end
defp unmask(key, <<data::16>>, acc) do
<<key::16, _::16>> = <<key::32>>
unmask(key, <<>>, <<acc::binary, bxor(data, key)::16>>)
end
defp unmask(key, <<data::8>>, acc) do
<<key::8, _::24>> = <<key::32>>
unmask(key, <<>>, <<acc::binary, bxor(data, key)::8>>)
end
defp unmask(_, <<>>, acc) do
acc
end
@spec recv(t, boolean, non_neg_integer, Keyword.t()) :: {:ok, binary} | {:error, error}
defp recv(%W{socket: socket, version: 13}, mask, length, options) do
length =
cond do
length == 127 ->
case socket |> GSMLG.Socket.Stream.recv(8, options) do
{:ok, <<length::64>>} ->
length
{:error, reason} ->
{:error, reason}
end
length == 126 ->
case socket |> GSMLG.Socket.Stream.recv(2, options) do
{:ok, <<length::16>>} ->
length
{:error, reason} ->
{:error, reason}
end
length <= 125 ->
length
end
case length do
{:error, reason} ->
{:error, reason}
length ->
if mask do
case socket |> GSMLG.Socket.Stream.recv(4, options) do
{:ok, <<key::32>>} ->
if length > 0 do
case socket |> GSMLG.Socket.Stream.recv(length, options) do
{:ok, data} ->
{:ok, unmask(key, data)}
{:error, reason} ->
{:error, reason}
end
else
{:ok, ""}
end
{:error, reason} ->
{:error, reason}
end
else
if length > 0 do
case socket |> GSMLG.Socket.Stream.recv(length, options) do
{:ok, data} ->
{:ok, data}
{:error, reason} ->
{:error, reason}
end
else
{:ok, ""}
end
end
end
end
defmacrop on_success(result, options) do
quote do
case recv(var!(self), var!(mask) == 1, var!(length), unquote(options)) do
{:ok, var!(data)} ->
{:ok, unquote(result)}
{:error, reason} ->
{:error, reason}
end
end
end
@doc """
Receive a packet from the websocket.
"""
@spec recv(t, Keyword.t()) :: {:ok, packet} | {:error, error}
def recv(self, options \\ [])
def recv(%W{socket: socket, version: 13} = self, options) do
case socket |> GSMLG.Socket.Stream.recv(2, options) do
# a non fragmented message packet
{:ok, <<1::1, 0::3, opcode::4, mask::1, length::7>>} when known?(opcode) and data?(opcode) ->
case on_success({opcode(opcode), data}, options) do
{:ok, {:text, data}} = result ->
if String.valid?(data) do
result
else
{:error, :invalid_payload}
end
{:ok, {:binary, _}} = result ->
result
end
# beginning of a fragmented packet
{:ok, <<0::1, 0::3, opcode::4, mask::1, length::7>>}
when known?(opcode) and not control?(opcode) ->
{:fragmented, opcode(opcode), data} |> on_success(options)
# a fragmented continuation
{:ok, <<0::1, 0::3, 0::4, mask::1, length::7>>} ->
{:fragmented, :continuation, data} |> on_success(options)
# final fragmented packet
{:ok, <<1::1, 0::3, 0::4, mask::1, length::7>>} ->
{:fragmented, :end, data} |> on_success(options)
# control packet
{:ok, <<1::1, 0::3, opcode::4, mask::1, length::7>>}
when known?(opcode) and control?(opcode) ->
case opcode(opcode) do
:ping ->
{:ping, data}
:pong ->
{:pong, data}
:close ->
case data do
<<>> ->
:close
<<code::16, rest::binary>> ->
{:close, close_code(code), rest}
end
end
|> on_success(options)
{:ok, nil} ->
# 1006 is reserved for connection closed with no close frame
# https://tools.ietf.org/html/rfc6455#section-7.4.1
{:ok, {:close, close_code(1006), nil}}
{:ok, _} ->
{:error, :protocol_error}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Receive a packet from the websocket, raising if an error occurs.
"""
@spec recv!(t, Keyword.t()) :: packet | no_return
def recv!(self, options \\ []) do
case recv(self, options) do
{:ok, packet} ->
packet
{:error, :protocol_error} ->
raise RuntimeError, message: "protocol error"
{:error, code} ->
raise GSMLG.Socket.Error, reason: code
end
end
@spec length(binary) :: binary
defp length(data) when byte_size(data) <= 125 do
<<byte_size(data)::7>>
end
defp length(data) when byte_size(data) <= 65536 do
<<126::7, byte_size(data)::16>>
end
defp length(data) when byte_size(data) <= 18_446_744_073_709_551_616 do
<<127::7, byte_size(data)::64>>
end
@spec forge(nil | true | integer, binary) :: binary
defp forge(nil, data) do
<<0::1, length(data)::bitstring, data::bitstring>>
end
defp forge(true, data) do
{key, data} = mask(data)
<<1::1, length(data)::bitstring, key::32, data::bitstring>>
end
defp forge(key, data) do
{key, data} = mask(key, data)
<<1::1, length(data)::bitstring, key::32, data::bitstring>>
end
@doc """
Send a packet to the websocket.
"""
@spec send(t, packet) :: :ok | {:error, error}
@spec send(t, packet, Keyword.t()) :: :ok | {:error, error}
def send(self, packet, options \\ [])
def send(%W{socket: socket, version: 13, mask: mask}, {opcode, data}, options)
when opcode != :close do
mask = if Keyword.has_key?(options, :mask), do: options[:mask], else: mask
socket
|> GSMLG.Socket.Stream.send(<<1::1, 0::3, opcode(opcode)::4, forge(mask, data)::binary>>)
end
def send(%W{socket: socket, version: 13, mask: mask}, {:fragmented, :end, data}, options) do
mask = if Keyword.has_key?(options, :mask), do: options[:mask], else: mask
socket |> GSMLG.Socket.Stream.send(<<1::1, 0::3, 0::4, forge(mask, data)::binary>>)
end
def send(
%W{socket: socket, version: 13, mask: mask},
{:fragmented, :continuation, data},
options
) do
mask = if Keyword.has_key?(options, :mask), do: options[:mask], else: mask
socket |> GSMLG.Socket.Stream.send(<<0::1, 0::3, 0::4, forge(mask, data)::binary>>)
end
def send(%W{socket: socket, version: 13, mask: mask}, {:fragmented, opcode, data}, options) do
mask = if Keyword.has_key?(options, :mask), do: options[:mask], else: mask
socket
|> GSMLG.Socket.Stream.send(<<0::1, 0::3, opcode(opcode)::4, forge(mask, data)::binary>>)
end
@doc """
Send a packet to the websocket, raising if an error occurs.
"""
@spec send!(t, packet) :: :ok | no_return
@spec send!(t, packet, Keyword.t()) :: :ok | no_return
def send!(self, packet, options \\ []) do
case send(self, packet, options) do
:ok ->
:ok
{:error, code} ->
raise GSMLG.Socket.Error, reason: code
end
end
@doc """
Send a ping request with the optional cookie.
"""
@spec ping(t) :: :ok | {:error, error}
@spec ping(t, binary) :: :ok | {:error, error}
def ping(self, cookie \\ :crypto.strong_rand_bytes(32)) do
case send(self, {:ping, cookie}) do
:ok ->
cookie
{:error, reason} ->
{:error, reason}
end
end
@doc """
Send a ping request with the optional cookie, raising if an error occurs.
"""
@spec ping!(t) :: :ok | no_return
@spec ping!(t, binary) :: :ok | no_return
def ping!(self, cookie \\ :crypto.strong_rand_bytes(32)) do
send!(self, {:ping, cookie})
cookie
end
@doc """
Send a pong with the given (and received) ping cookie.
"""
@spec pong(t, binary) :: :ok | {:error, error}
def pong(self, cookie) do
send(self, {:pong, cookie})
end
@doc """
Send a pong with the given (and received) ping cookie, raising if an error
occurs.
"""
@spec pong!(t, binary) :: :ok | no_return
def pong!(self, cookie) do
send!(self, {:pong, cookie})
end
@doc """
Close the socket when a close request has been received.
"""
@spec close(t) :: :ok | {:error, error}
def close(%W{socket: socket, version: 13}) do
socket
|> GSMLG.Socket.Stream.send(<<1::1, 0::3, opcode(:close)::4, forge(nil, <<>>)::binary>>)
end
@doc """
Close the socket sending a close request, unless `:wait` is set to `false` it
blocks until the close response has been received, and then closes the
underlying socket.
If :reason? is set to true and the response contains a closing reason
and custom data the function returns it as a tuple.
"""
@spec close(t, atom, Keyword.t()) :: :ok | {:ok, atom, binary} | {:error, error}
def close(%W{socket: socket, version: 13, mask: mask} = self, reason, options \\ []) do
{reason, data} = if is_tuple(reason), do: reason, else: {reason, <<>>}
mask = if Keyword.has_key?(options, :mask), do: options[:mask], else: mask
socket
|> GSMLG.Socket.Stream.send(
<<1::1, 0::3, opcode(:close)::4,
forge(
mask,
<<close_code(reason)::16, data::binary>>
)::binary>>
)
unless options[:wait] == false do
do_close(self, recv(self, options), Keyword.get(options, :reason?, false), options)
end
end
defp do_close(self, {:ok, :close}, _, _) do
abort(self)
end
defp do_close(self, {:ok, {:close, _, _}}, false, _) do
abort(self)
end
defp do_close(self, {:ok, {:close, reason, data}}, true, _) do
abort(self)
{:ok, reason, data}
end
defp do_close(self, {:ok, {:error, _}}, _, _) do
abort(self)
end
defp do_close(self, _, reason?, options) do
do_close(self, recv(self, options), reason?, options)
end
@doc """
Close the underlying socket, only use when you mean it, normal closure
procedure should be preferred.
"""
@spec abort(t) :: :ok | {:error, error}
def abort(%W{socket: socket}) do
GSMLG.Socket.Stream.close(socket)
end
end