diff options
Diffstat (limited to 'lib/irc/connection.ex')
-rw-r--r-- | lib/irc/connection.ex | 443 |
1 files changed, 228 insertions, 215 deletions
diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex index 052141c..7b82cea 100644 --- a/lib/irc/connection.ex +++ b/lib/irc/connection.ex @@ -1,7 +1,9 @@ defmodule Irc.Connection do @behaviour :gen_statem require Logger + alias Irc.ConnectionSocket alias Irc.Parser.Line + alias Irc.Parser require Irc.Parser.Numeric import Irc.Parser.Numeric @@ -20,9 +22,10 @@ defmodule Irc.Connection do * the socket and reconnections, * registration process, * sasl authentication, - * capabilities negotiation, + * capabilities negotiation/lifecycle, * ping, - * nick and user mode changes. + * nick and user mode changes, + * batches/echo. Events and lines are sent to calling PID. @@ -43,13 +46,35 @@ defmodule Irc.Connection do If you are looking for a more featured client, you probably want `Irc.Client`! """ - defstruct [:server, :nick, :modes, :capabs, :version, :umodes, :cmodes, :cmodes_with_params, :isupport, :motd] + defstruct [:server, :host, :port, :tls, :sts, :nick, :modes, :capabs, :server_capabs, :version, :umodes, :cmodes, :cmodes_with_params, :isupport, :motd] - @typedoc "Connection Information state" + @typedoc """ + Connection Information state. Retrieve it with `info/1`. + + * `server`: reported server name, + * `host`: connection host, + * `port`: connection port, + * `tls`: connection using tls, + * `sts`: connection using sts, + * `nick`: current connection nickname, + * `capabs`: negotiated capabilities, + * `server_capabs`: server offerred capabilities (with their parameters if any), + * `modes`: connection modes, + * `version`: reported server version, + * `umodes`: user modes, + * `cmodes` and `cmodes_with_params`: channel modes, + * `isupport`: server ISUPPORT, see `Irc.Parser.Isupport`, + * `motd`: MOTD. + """ @type t :: %__MODULE__{ server: String.t, + host: String.t, + port: Integer.t, + tls: boolean, + sts: boolean, nick: String.t, - capabs: list(), + capabs: [String.t], + server_capabs: Irc.Parser.Capabs.t(), modes: String.t, version: String.t, umodes: String.t, @@ -86,6 +111,10 @@ defmodule Irc.Connection do | {:quit, reason :: String.t} | {:irc_error, reason :: String.t} + # TODO: Better batch / Echo-message / message id + # TODO: CAP lifecycle while connection is established + # TODO: SASL + # TODO: supports?/capab? by calling PID, more info to retrieve this way? @typedoc """ Start options. @@ -95,8 +124,9 @@ defmodule Irc.Connection do * pass: IRC server password, * tls, * port (default 6667, 6697 if tls), + * sts, see `t:start_opt_sts/0`, * reconnect (automatic/backoff reconnection), - * [batch][batch] + * [batch][batch] option, see `t:start_opt_batch/0`, * [sasl][sasl] options, see `t:start_opt_sasl/0`, * [webirc][webirc] options, see `t:start_opt_webirc/0`, * capabs: list to request, @@ -155,6 +185,7 @@ defmodule Irc.Connection do Send a [WEBIRC][webirc] command upon connection. ``` + {password, gateway, user_hostname, user_port, flags} {"hunter5", "web.irc.mynetwork", "some-user-hostname", "127.0.0.1", %{"secure" => true}} ``` @@ -238,10 +269,10 @@ defmodule Irc.Connection do @spec capab?(t, String.t) :: boolean @doc "Returns true if the capab `feature` has been negotiated for this connection." def capab?(%__MODULE__{capabs: capabs}, feature) do - Enum.member?(capabs, feature) + Map.has_key?(capabs, feature) end - @spec supports?(String.t, t) :: boolean + @spec supports?(t, String.t) :: boolean @doc "Returns true if `feature` is reported as supported by the server (ISUPPORT)." def supports?(%__MODULE__{isupport: isupport}, feature) do Map.has_key?(isupport, String.upcase(feature)) @@ -261,10 +292,17 @@ defmodule Irc.Connection do end end + # + # --- STATE MACHINE + # + # * connect -> [reg_webirc -> cap_ls -> reg_nick -> reg_pass -> cap_req -> cap_wait_ack -> welcome -> motd] + # -> [disconnected, registered] + # * disconnected -> [connect] + defmodule State do @moduledoc false defstruct [:args, - :backoff, :socket, :sockmodule, + :backoff, :socket, :process, :monitor, :nick, :server, @@ -273,16 +311,19 @@ defmodule Irc.Connection do :cmodes, :cmodes_with_params, :welcomed, - :sts, :port, :tls, - {:batches, %{}}, - {:errored, false}, - {:isupport, %{}}, + :tls, + :cap_302, # If server supports 302 + :sts, # If an STS policy has been used + :port, # Actual connection port (may differ than args.port due to STS) + {:batches, %{}}, # Active batches + {:errored, false}, # TODO + {:isupport, %{}}, # Server ISUPPORT {:modes, []}, - {:server_capabs, []}, - {:client_capabs, []}, - {:capabs, []}, + {:server_capabs, %{}}, # Server advertised capabs + {:client_capabs, []}, # Client enabled capabs + {:capabs, []}, # Negotiated capabs {:motd, []}, - {:welcome, []}, + {:welcome, []}, # Registration log ] end @@ -291,21 +332,17 @@ defmodule Irc.Connection do @doc false def init([args]) do - backoff_min = Keyword.get(args, :backoff_min, @backoff_min) - backoff_max = Keyword.get(args, :backoff_max, @backoff_max) - process = Keyword.get(args, :process, nil) + backoff_min = Map.get(args, :backoff_min, @backoff_min) + backoff_max = Map.get(args, :backoff_max, @backoff_max) + process = Map.get(args, :process, nil) monitor = Process.monitor(process) with \ {:ok, args} <- process_args(args), backoff <- :backoff.init(backoff_min, backoff_max) do - {tls, sts, port} = if !args.tls && args.sts do - case Irc.STS.lookup(args.host, args.port) do - {true, port} -> {true, true, port} - {false, port} -> {false, false, port} - end - else - {args.tls, false, args.port} + {tls, sts, port} = case {args.sts, Irc.STS.lookup(args.host, args.port)} do + {true, {:ok, port}} -> {true, true, port} + _ -> {args.tls, false, args.port} end client_capabs = [Irc.Parser.capabs(), @connection_capabs, @@ -335,17 +372,13 @@ defmodule Irc.Connection do # TCP Open @doc false - def connect(:internal, _, data = %{tls: true}) do - {:next_state, :connect_tls, data, {:next_event, :internal, nil}} - end - def connect(:internal, _, data) do Logger.debug("#{inspect(data)} connecting...") options = [{:active, true}, {:packet, :line}] ++ data.args.connect_opts - case :gen_tcp.connect(String.to_charlist(data.args.host), data.port, options) do + case ConnectionSocket.connect(data.tls, data.args.host, data.port, options) do {:ok, socket} -> {_, backoff} = :backoff.succeed(data.backoff) - {:next_state, :connected, %State{data | backoff: backoff}, {:next_event, :internal, {:gen_tcp, socket}}} + {:next_state, :reg_webirc, %State{data | socket: socket, backoff: backoff}, @internal} error -> # TODO ERRORS to_disconnected(error, data) @@ -356,45 +389,11 @@ defmodule Irc.Connection do handle_common(:connect, type, content, data) end - # TLS Connect - - @doc false - def connect_tls(:internal, nil, data) do - options = [{:active, true}, {:packet, :line}] ++ data.args.connect_opts - case :ssl.connect(String.to_charlist(data.args.host), data.port, options) do - {:ok, socket} -> - {_, backoff} = :backoff.succeed(data.backoff) - {:next_state, :connected, %State{data | backoff: backoff}, {:next_event, :internal, {:ssl, socket}}} - error -> - to_disconnected(error, data) - end - end - - def connect_tls(type, content, data) do - handle_common(:connect_tls, type, content, data) - end - - @doc false - def connected(:internal, {module, socket}, data) do - next = if Map.get(data.args, :webirc) do - :webirc - else - :cap_ls - end - - {:next_state, next, %State{data | sockmodule: module, socket: socket}, @internal} - end - - def connected(type, content, data) do - handle_common(:connected, type, content, data) - end - # Registration: WEBIRC # https://ircv3.net/specs/extensions/webirc @doc false - def webirc(:internal, _, data = %{args: args}) do - {password, gateway, hostname, ip, options} = Keyword.get(args, :webirc) + def reg_webirc(:internal, _, data = %{args: %{webirc: {password, gateway, hostname, ip, options}}}) do opt_string = Enum.map(options, fn({key, value}) -> if value == true do key @@ -409,8 +408,12 @@ defmodule Irc.Connection do end end - def webirc(type, content, data) do - handle_common(:webirc, type, content, data) + def reg_webirc(:internal, _, data) do + {:next_state, :cap_ls, data, @internal} + end + + def reg_webirc(type, content, data) do + handle_common(:reg_webirc, type, content, data) end # Registration: LIST CAPABS @@ -420,61 +423,56 @@ defmodule Irc.Connection do case socksend(data, 'CAP LS 320') do :ok -> {:keep_state_and_data, {:state_timeout, @cap_timeout, nil}} error -> - Logger.error("Disconnected in socksend") + Logger.error("#{inspect data} - cap_ls: Disconnected in socksend") to_disconnected(error, data) end end - def cap_ls(:info, msg = {module, socket, line}, data) do - case Line.parse(line) do - l = %Line{command: "CAP", args: [_, "LS", "*", rest]} -> - capabs = String.split(rest, " ") - {:keep_state, %State{data | server_capabs: data.capabs ++ capabs}, {:state_timeout, @reply_timeout, nil}} - l = %Line{command: "CAP", args: ["*", "LS", rest]} -> - capabs = Enum.uniq(data.capabs ++ String.split(rest, " ")) - {:next_state, :cap_sts, %State{data | server_capabs: capabs}, @internal} - l -> - handle_common(:cap_ls, :info, msg, data) + # multiline capab + def cap_ls(:info, {_, _, %Line{command: "CAP", args: [_, "LS", "*", rest]}}, data) do + capabs = Map.merge(data.server_capabs, Parser.Capabs.parse(rest)) + {:keep_state, %State{data | server_capabs: capabs, cap_302: true}, {:state_timeout, @reply_timeout, nil}} + end + + # end of multiline or no multiline indicator + def cap_ls(:info, {_, _, %Line{command: "CAP", args: [_, "LS", rest]}}, data) do + capabs = Map.merge(data.server_capabs, Parser.Capabs.parse(rest)) + if data.cap_302 do + {:next_state, :cap_sts, %State{data | server_capabs: capabs}, @internal} + else + {:keep_state, %State{data | server_capabs: capabs}, {:state_timeout, @reply_timeout, nil}} end end + # state timeout - consider LS complete def cap_ls(:state_timeout, _, data) do - Logger.debug("State timeout") + Logger.debug("#{inspect data} - CAP LS State timeout") {:next_state, :cap_sts, data, @internal} end def cap_ls(type, content, data) do - Logger.debug("Handling common #{inspect({type,content,data})}") handle_common(:cap_ls, type, content, data) end # REGISTRATION: Check for STS + # STS is not already enabled on this connection, and a policy is advertised with a port: follow the policy and + # reconnect. @doc false - def cap_sts(:internal, _, data = %{args: %{sts: true}}) do - sts? = Enum.find(data.capabs, fn(capab) -> String.starts_with?(capab, "sts=") end) - if sts? do - "sts="<>rules = sts? - rules = Map.reduce(String.split(rules, ","), %{}, fn(k_v, acc) -> - [k, v] = String.split(k_v, "=") - Map.put(acc, k, v) - end) - port = Map.get(rules, "port") - duration = Map.get(rules, "duration") - Irc.STS.witness(data.args.host, data.args.port, port, Map.get(rules, "duration")) - if !data.tls && port && duration do - Logger.info("#{inspect data} STS advertised, reconnecting to tls port #{port}") - socksend(data, ['QUIT :STS, Reconnecting']) - data.sockmodule.close(data.socket) - {:next_state, :connect, %State{data | sts: true, port: port, tls: true, socket: nil, sockmodule: nil}, @internal} - else - {:next_state, :reg_pass, %State{data | sts: true}, @internal} - end - else - {:next_state, :reg_pass, data, @internal} - end + def cap_sts(:internal, _, data = %{args: %{sts: true}, sts: false, port: current_port, + server_capabs: %{"sts" => policy = %{"port" => sts_port}}}) when current_port != sts_port do + socksend(data, Line.new("QUIT", ["Following STS policy."])) + ConnectionSocket.close(data.socket) + {:next_state, :connect, %State{data | sts: true, port: sts_port, tls: true, socket: nil}, @internal} + end + + # STS is enabled and we are over TLS, witness the policy. + def cap_sts(:internal, _, data = %{args: %{sts: true}, sts: true, tls: true, server_capabs: %{"sts" => policy}}) do + Irc.STS.witness(data.args.host, policy) + {:next_state, :reg_pass, data, @internal} end + # invalid conditions for sts, exit state def cap_sts(:internal, _, data) do {:next_state, :reg_pass, data, @internal} end @@ -487,7 +485,7 @@ defmodule Irc.Connection do @doc false def reg_pass(:internal, _, data = %State{args: %{pass: pass}}) when is_binary(pass) and pass != "" do - case socksend(data, ['PASS ', pass]) do + case socksend(data, Line.new("PASS", pass)) do :ok -> {:keep_state_and_data, {:state_timeout, @reply_timeout, nil}} error -> @@ -499,14 +497,8 @@ defmodule Irc.Connection do {:next_state, :reg_nick, data, @internal} end - def reg_pass(:info, content = {_, _, line}, data) do - case Line.parse(line) do - %Line{command: err_PASSWDMISMATCH()} -> - {:next_state, :error, {:invalid_password, data.args.pass}} - line -> - #{:keep_state_and_data, [:postpone, {:state_timeout, @reply_timeout, nil}]} - handle_common(:reg_pass, :info, content, data) - end + def reg_pass(:info, {_, _, %Line{command: err_PASSWDMISMATCH()}}, data) do + {:next_state, :error, {:invalid_password, data.args.pass}} end def reg_pass(:state_timeout, _, data) do @@ -521,23 +513,19 @@ defmodule Irc.Connection do @doc false def reg_nick(:internal, nick, data) do - case socksend(data, ['NICK ', nick || data.args.nick]) do + case socksend(data, Line.new("NICK", nick || data.args.nick)) do :ok -> {:keep_state, %State{data | nick: nick}, {:state_timeout, @reply_timeout, nil}} error -> to_disconnected(error, data) end end - def reg_nick(:info, content = {_, _, line}, data) do - case Line.parse(line) do - %Line{command: cmd} when cmd in [err_NICKNAMEINUSE(), err_NICKCOLLISION()] -> - new_nick = data.args.nick <> to_string(:random.uniform(9999)) - {:keep_state, data, {:next_event, :internal, new_nick}} - line = %Line{command: cmd} when cmd in [err_ERRONEUSNICKNAME(), err_NONICKNAMEGIVEN()] -> - {:next_state, :error, data, {:next_event, :internal, {:invalid_nickname, data.args.nick, line}}} - line -> - #{:keep_state_and_data, :postpone} - handle_common(:reg_nick, :info, content, data) - end + def reg_nick(:info, {_, _, %Line{command: cmd}}, data) when cmd in [err_NICKNAMEINUSE(), err_NICKCOLLISION() ]do + new_nick = data.args.nick <> to_string(:random.uniform(9999)) + {:keep_state, data, {:next_event, :internal, new_nick}} + end + + def reg_nick(:info, {_, _, line = %Line{command: cmd}}, data) when cmd in [err_ERRONEUSNICKNAME(), err_NONICKNAMEGIVEN()] do + {:next_state, :error, data, {:next_event, :internal, {:invalid_nickname, data.args.nick, line}}} end def reg_nick(:state_timeout, _, data) do @@ -552,8 +540,7 @@ defmodule Irc.Connection do @doc false def reg_user(:internal, _, data) do - user = List.flatten(['USER ', data.args.user, ' 0 * :', data.args.name]) - case socksend(data, user) do + case socksend(data, Line.new("USER", [data.args.user, "0", "*", data.args.name])) do :ok -> {:next_state, :cap_req, data, @internal} error -> @@ -561,24 +548,23 @@ defmodule Irc.Connection do end end - #def reg_user(:state_timeout, _, data) do - # {:next_state, :welcome, data, {:state_timeout, @registration_timeout, nil}} - #end - def reg_user(type, content, data) do handle_common(:reg_user, type, content, data) end + # --- Registration: CAP REQ + @doc false - def cap_req(:internal, _, data = %{server_capabs: []}) do + # No advertised server capabs, skip to WELCOME phase. + def cap_req(:internal, _, data = %{server_capabs: server_capabs}) when server_capabs == %{} do {:next_state, :welcome, data} end - def cap_req(:internal, _, data) do - req = Enum.filter(data.client_capabs, fn(capab) -> Enum.member?(data.server_capabs, capab) end) + def cap_req(:internal, _, data = %{client_capabs: client_capabs, server_capabs: server_capabs}) do + req = Enum.filter(client_capabs, fn(capab) -> Map.has_key?(server_capabs, capab) end) |> Enum.uniq() |> Enum.join(" ") - case socksend(data, ['CAP REQ :', String.to_charlist(req)]) do + case socksend(data, Line.new("CAP", args: ["REQ", req])) do :ok -> {:next_state, :cap_wait_ack, data} error -> to_disconnected(error, data) end @@ -588,23 +574,23 @@ defmodule Irc.Connection do handle_common(:cap_req, type, content, data) end + # --- Registration: CAP ACK/NACK + @doc false def cap_wait_ack(:enter, _, _) do {:keep_state_and_data, {:state_timeout, @reply_timeout, nil}} end - # FIXME: Support multiline ACK/NACK - def cap_wait_ack(:info, content = {_, _, line}, data) do - case Line.parse(line) do - %Line{command: "CAP", args: [_, "ACK", capabs]} -> - capabs = String.split(capabs, " ") - {:keep_state, %State{data | capabs: capabs}, {:next_event, :internal, :end}} - %Line{command: "CAP", args: [_, "NACK", capabs]} -> - capabs = String.split(capabs, " ") - {:keep_state, %State{data | capabs: data.capabs -- capabs}, {:next_event, :internal, :end}} - line -> - handle_common(:cap_wait_ack, :info, content, data) - end + # FIXME: Support multiline ACK/NACK ??? + # TODO: VERIFY SPEC CONFORMITY + def cap_wait_ack(:info, {_, _, %Line{command: "CAP", args: [_, "ACK", capabs]}}, data) do + capabs = String.split(capabs, " ") + {:keep_state, %State{data | capabs: capabs}, {:next_event, :internal, :end}} + end + + def cap_wait_ack(:info, {_, _, %Line{command: "CAP", args: [_, "NACK", capabs]}}, data) do + capabs = String.split(capabs, " ") + {:keep_state, %State{data | capabs: data.capabs -- capabs}, {:next_event, :internal, :end}} end def cap_wait_ack(:internal, :end, data) do @@ -614,6 +600,10 @@ defmodule Irc.Connection do end end + def cap_wait_ack(type, message, data) do + handle_common(:cap_wait_ack, type, message, data) + end + # --- REGISTRATION: Welcome @doc false @@ -621,30 +611,30 @@ defmodule Irc.Connection do {:keep_state_and_data, {:state_timeout, @registration_timeout, nil}} end + def welcome(:info, {_, _, l = %Line{command: rpl_WELCOME(), source: source, args: [nick, _]}}, data) do + {:keep_state, %State{data | nick: nick, welcome: [l | data.welcome], server: source}, {:state_timeout, @reply_timeout, nil}} + end + @welcome_numeric [rpl_WELCOME, rpl_YOURHOST, rpl_CREATED, rpl_MYINFO, rpl_ISUPPORT] - def welcome(:info, content = {module, socket, line}, data) do - case Line.parse(line) do - l = %Line{command: err_NOMOTD()} -> - {:next_state, :registered, %State{data | motd: []}} - l = %Line{command: rpl_MOTDSTART()} -> - {:next_state, :motd, data, [:postpone]} - l = %Line{command: rpl_WELCOME(), source: source, args: [nick, _]} -> - {:keep_state, %State{data | nick: nick, welcome: [l | data.welcome], server: source}, {:state_timeout, @reply_timeout, nil}} - l = %Line{command: rpl_MYINFO(), source: source, args: [_, _, version, umodes, cmodes | rest]} -> - cmodes_with_params = case rest do - [] -> nil - [c] -> c - end - data = %State{data | welcome: [l | data.welcome], version: version, umodes: umodes, - cmodes: cmodes, cmodes_with_params: cmodes_with_params} - {:keep_state, data, {:state_timeout, @reply_timeout, nil}} - l = %Line{command: rpl_ISUPPORT(), args: [_ | isupport]} -> - supports = Irc.Parser.Isupport.parse(isupport, data.isupport) - {:keep_state, %State{data | isupport: supports}, {:state_timeout, @reply_timeout, nil}} - l = %Line{command: cmd} -> - #{:keep_state, %State{data | welcome: [l | data.welcome]}, {:state_timeout, @reply_timeout, nil}} - handle_common(:welcome, :info, content, data) + + def welcome(:info, {_, _, l = %Line{command: rpl_MYINFO(), source: source, args: [_, _, version, umodes, cmodes | rest]}}, data) do + cmodes_with_params = case rest do + [] -> nil + [c] -> c end + data = %State{data | welcome: [l | data.welcome], version: version, umodes: umodes, + cmodes: cmodes, cmodes_with_params: cmodes_with_params} + {:keep_state, data, {:state_timeout, @reply_timeout, nil}} + end + + def welcome(:info, {_, _, %Line{command: rpl_ISUPPORT(), args: [_ | isupport]}}, data) do + supports = Irc.Parser.Isupport.parse(isupport, data.isupport) + {:keep_state, %State{data | isupport: supports}, {:state_timeout, @reply_timeout, nil}} + end + + # No MOTD/Start of MOTD ends the welcome phase + def welcome(:info, {_, _, %Line{command: cmd}}, data) when cmd in [err_NOMOTD(), rpl_MOTDSTART()] do + {:next_state, :motd, data, [:postpone]} end def welcome(:state_timeout, _, data) do @@ -662,19 +652,20 @@ defmodule Irc.Connection do {:keep_state_and_data, {:state_timeout, @reply_timeout, nil}} end - def motd(:info, {_, _, line}, data) do - case Line.parse(line) do - %Line{command: rpl_MOTDSTART(), args: [_, line]} -> - {:keep_state, %State{data | motd: [line]}, {:state_timeout, @reply_timeout, nil}} - %Line{command: err_NOMOTD()} -> - {:next_state, :registered, %State{data | motd: []}} - %Line{command: rpl_MOTD(), args: [_, line]} -> - {:keep_state, %State{data | motd: [line | data.motd]}, {:state_timeout, @reply_timeout, nil}} - %Line{command: rpl_ENDOFMOTD(), args: [_, line]} -> - {:next_state, :registered, %State{data | motd: [line | data.motd]}} - line -> - {:keep_state_and_data, :postpone} - end + def motd(:info, {_, _, %Line{command: err_NOMOTD()}}, data) do + {:next_state, :registered, %State{data | motd: []}} + end + + def motd(:info, {_, _, %Line{command: rpl_MOTDSTART(), args: [_, line]}}, data) do + {:keep_state, %State{data | motd: [line]}, {:state_timeout, @reply_timeout, nil}} + end + + def motd(:info, {_, _, %Line{command: rpl_MOTD(), args: [_, line]}}, data) do + {:keep_state, %State{data | motd: [line | data.motd]}, {:state_timeout, @reply_timeout, nil}} + end + + def motd(:info, {_, _, %Line{command: rpl_ENDOFMOTD(), args: [_, line]}}, data) do + {:next_state, :registered, %State{data | motd: List.reverse([line | data.motd])}} end def motd(:state_timeout, _, data) do @@ -690,7 +681,8 @@ defmodule Irc.Connection do defp to_info(data, state \\ nil) do data |> Map.from_struct() - |> Map.take([:nick, :modes, :server, :version, :umodes, :cmodes, :cmodes_with_params, :capabs, :isupport, :motd]) + |> Map.take([:nick, :port, :tls, :sts, :modes, :server, :version, :umodes, :cmodes, :cmodes_with_params, :capabs, :isupport, :motd]) + |> Map.put(:host, data.args.host) |> Map.put(:__struct__, __MODULE__) end @@ -710,7 +702,6 @@ defmodule Irc.Connection do end def registered(:info, {_, _, line}, data) do - line = Line.parse(line) Logger.debug "#{inspect(data)} <<< #{inspect(line)}" case process_line(line, data) do :ignore -> @@ -843,8 +834,8 @@ defmodule Irc.Connection do {delay, backoff} = :backoff.fail(data.backoff) send(data.process, {:irc_conn_down, self(), error, delay}) data = if data.socket do - data.sockmodule.close(data.socket) - %State{data | socket: nil, sockmodule: nil} + ConnectionSocket.close(data.socket) + %State{data | socket: nil} else data end @@ -873,7 +864,7 @@ defmodule Irc.Connection do :keep_state_and_data end - def disconnected(:info, {closed, socket}, data) when closed in [:tcp_closed, :ssl_closed] do + def disconnected(:info, {ConnectionSocket, _socket, :closed}, data) do :keep_state_and_data end @@ -898,15 +889,15 @@ defmodule Irc.Connection do def handle_common(_, {:call, ref}, {:disconnect, stop, quit}, data = %State{socket: socket}) do Logger.debug("#{inspect(data)} disconnecting on request (stop:#{inspect stop})") - if data.sockmodule && data.socket do + if data.socket do quit = if quit, do: quit, else: "" socksend(data, ['QUIT :', quit]) - data.sockmodule.close(socket) + ConnectionSocket.close(socket) end if stop do {:stop_and_reply, :normal, {:reply, ref, :ok}} else - {:next_state, :disconnected, %State{data | socket: nil, sockmodule: nil}, [ + {:next_state, :disconnected, %State{data | socket: nil}, [ {:reply, ref, :ok}, {:next_event, :internal, :disconnected}, ]} @@ -936,18 +927,16 @@ defmodule Irc.Connection do :keep_state_and_data end - def handle_common(_, :info, {_mod, _sock, line = [?P, ?I, ?N, ?G | _]}, data) do - line = Line.parse(line) - arg = Enum.join(line.args, " ") + def handle_common(_, :info, {_, _, %Line{command: "PING", args: args}}, data) do + arg = Enum.join(args, " ") case socksend(data, ['PONG :', arg]) do :ok -> :keep_state_and_data error -> to_disconnected(error, data) end end - def handle_common(_, :info, {_mod, _sock, line = [?E, ?R, ?R, ?O, ?R | _]}, data) do - line = Line.parse(line) - to_disconnected({:irc_error, Enum.join(line.args, " ")}, data) + def handle_common(_, :info, {_, _, %Line{command: "ERROR", args: args}}, data) do + to_disconnected({:irc_error, Enum.join(args, " ")}, data) end @@ -955,12 +944,29 @@ defmodule Irc.Connection do {:keep_state_and_data, :postpone} end - def handle_common(_, :info, {closed, socket}, data) when closed in [:tcp_closed, :ssl_closed] do - {:next_state, :disconnected, %State{data | socket: nil, sockmodule: nil}, @internal} + # TODO: SOCKET ERROR + def handle_common(_, :info, {ConnectionSocket, _socket, :closed}, data) do + Logger.error("#{inspect data} - socket closed") + {:next_state, :disconnected, %State{data | socket: nil}, @internal} + end + + # SOCKET DIED + def handle_common(_, :info, {:DOWN, ref, :process, socket_pid, reason}, data = %{socket: {ConnectionSocket,socket_pid,ref}}) do + Logger.error("#{inspect data} - Socket pid died #{inspect reason}") + {:next_state, :disconnected, %State{data | socket: nil}, @internal} + end + + # OWNER DIED + def handle_common(_, :info, {:DOWN, _, :process, pid, reason}, data = %{process: pid}) do + Logger.error("#{inspect data} - Owner pid died #{inspect reason}") + {:stop, :normal, data} end @doc false def terminate(reason, state, data) do + if state.socket do + ConnectionSocket.close(state.socket) + end unless data.errored do send(data.process, {:irc_conn_error, self(), :down}) end @@ -974,11 +980,11 @@ defmodule Irc.Connection do # Send helpers. defp socksend(data, line = %Line{}) do - socksend(data, [Line.encode(line, info(data))]) + socksend(data, [Line.encode(line, to_info(data))]) end - defp socksend(data = %State{sockmodule: module, socket: socket}, line) when is_list(line) do + defp socksend(data = %State{socket: socket}, line) when is_list(line) do Logger.debug("#{inspect data} >>> #{inspect line}") - module.send(socket, [line | [?\r, ?\n]]) + ConnectionSocket.send(socket, [line | [?\r, ?\n]]) end @@ -995,26 +1001,28 @@ defmodule Irc.Connection do |> Keyword.put_new(:nick, nick) |> Keyword.put_new(:host, host) |> Keyword.put_new(:process, self()) + |> Enum.into(Map.new) end # Process args (in init) defp process_args(args) do - host = Keyword.get(args, :host, nil) - tls = Keyword.get(args, :tls, nil) || false - sts = Keyword.get(args, :sts, nil) || true + host = Map.get(args, :host) + tls = Map.get(args, :tls) || false + sts = Map.get(args, :sts) || true default_port = if tls, do: 6697, else: 6667 - port = Keyword.get(args, :port, nil) || default_port - connect_timeout = Keyword.get(args, :connect_timeout, @connect_timeout) - nick = Keyword.get(args, :nick, nil) - user = Keyword.get(args, :user, nick) || nick - name = Keyword.get(args, :name, user) || user - pass = Keyword.get(args, :pass, nil) - capabs = Keyword.get(args, :capabs, []) || [] - reconnect = Keyword.get(args, :reconnect, true) - batch = Keyword.get(args, :batch, true) - connect_opts = Keyword.get(args, :connect_opts, []) || [] + port = Map.get(args, :port) || default_port + connect_timeout = Map.get(args, :connect_timeout, @connect_timeout) + nick = Map.get(args, :nick) + user = Map.get(args, :user) || nick + name = Map.get(args, :name) || user + pass = Map.get(args, :pass) + capabs = Map.get(args, :capabs) || [] + reconnect = Map.get(args, :reconnect, true) + batch = Map.get(args, :batch, true) + connect_opts = Map.get(args, :connect_opts) || [] with \ + {:nick, :ok} <- {:nick, verify_nickname(nick)}, {:host, {:ok, host}} <- {:host, parse_host(host)}, {:port, {:ok, port}} <- {:port, parse_port(port)} do @@ -1048,6 +1056,11 @@ defmodule Irc.Connection do end defp parse_port(port), do: {:error, {:invalid_format, :port, inspect(port)}} + defp verify_nickname(nick) when is_binary(nick) and byte_size(nick) > 1 do + :ok + end + + defp verify_nickname(nick), do: {:error, {:invalid_nickname, nick}} defimpl Inspect, for: State do @moduledoc false |