summaryrefslogtreecommitdiff
path: root/lib/irc/connection.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/irc/connection.ex')
-rw-r--r--lib/irc/connection.ex443
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