summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhref <href@random.sh>2021-01-09 14:38:05 +0100
committerhref <href@random.sh>2021-01-09 14:38:05 +0100
commitc77be7ec8fbbbd2b871efa3822be3b25edeb445d (patch)
tree5f1fd8ba0b02e2ae37bbf8a7c28b1808999a7594
parent (diff)
Old WIP commit - STS, ...
-rw-r--r--README.md2
-rw-r--r--config/config.exs30
-rw-r--r--lib/irc/client/command/away.ex2
-rw-r--r--lib/irc/connection.ex443
-rw-r--r--lib/irc/connection_socket.ex100
-rw-r--r--lib/irc/parser/capabs.ex32
-rw-r--r--lib/irc/parser/line.ex20
-rw-r--r--lib/irc/sts.ex130
8 files changed, 472 insertions, 287 deletions
diff --git a/README.md b/README.md
index 4b81d33..57f19a4 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@ Examples of usage: `Irc.Shout` (a simple connect-join-message-quit on Connection
Future versions may include: bots, server parser, server-to-server connections, ….
+**DO NOT USE, it's all moving parts and they'll move and shuffle and move for a long time before being a bit stable!**
+
### Supported features
The eventual goal is to support most of the modern IRC specifications.
diff --git a/config/config.exs b/config/config.exs
index 36bd1d8..eaeaf25 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,30 +1,4 @@
-# This file is responsible for configuring your application
-# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
-# This configuration is loaded before any dependency and is restricted
-# to this project. If another project depends on this project, this
-# file won't be loaded nor affect the parent project. For this reason,
-# if you want to provide default values for your application for
-# third-party users, it should be done in your "mix.exs" file.
-
-# You can configure your application as:
-#
-# config :irc, key: :value
-#
-# and access this configuration in your application as:
-#
-# Application.get_env(:irc, :key)
-#
-# You can also configure a third-party app:
-#
-# config :logger, level: :info
-#
-
-# It is also possible to import configuration files, relative to this
-# directory. For example, you can emulate configuration per environment
-# by uncommenting the line below and defining dev.exs, test.exs and such.
-# Configuration from the imported file will override the ones defined
-# here (which is why it is important to import them last).
-#
-# import_config "#{Mix.env()}.exs"
+config :irc,
+ sts_store_file: {:priv, "sts_policies.dets"}
diff --git a/lib/irc/client/command/away.ex b/lib/irc/client/command/away.ex
index 3b6b38e..da8b5ef 100644
--- a/lib/irc/client/command/away.ex
+++ b/lib/irc/client/command/away.ex
@@ -15,7 +15,7 @@ defmodule Irc.Client.Command.Away do
{:send, command}
end
- def hanle_line(%Line{command: "AWAY", source: source, args: args}, _) do
+ def handle_line(%Line{command: "AWAY", source: source, args: args}, _) do
case args do
[] -> {:event, {:away, source}}
[message] -> {:event, {:away, {source, message}}}
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
diff --git a/lib/irc/connection_socket.ex b/lib/irc/connection_socket.ex
new file mode 100644
index 0000000..29c42c0
--- /dev/null
+++ b/lib/irc/connection_socket.ex
@@ -0,0 +1,100 @@
+defmodule Irc.ConnectionSocket do
+ use GenServer
+ require Logger
+ alias Irc.Parser.Line
+
+ import Kernel, except: [send: 2]
+
+ @moduledoc "Underlying wrapper that just parses IRC lines"
+
+ @type error :: :todo
+
+ @type t :: {ConnectionSocket, pid(), reference()}
+
+ @spec connect(tls :: boolean, host :: String.t, port :: Integer.t, options :: Keyword.t) :: {:ok, t} | {:error, error}
+ def connect(tls, host, port, options) do
+ caller = self()
+ case GenServer.start(__MODULE__, [caller, tls, host, port, options]) do
+ {:ok, pid} ->
+ mon = Process.monitor(pid)
+ {:ok, {__MODULE__, pid, mon}}
+ error -> error
+ end
+ end
+
+ @spec close(t) :: :ok
+ def close({__MODULE__, pid, mon}) do
+ case GenServer.call(pid, :close) do
+ :ok ->
+ Process.demonitor(mon, [:flush])
+ error ->
+ error
+ end
+ end
+
+ @spec send(t, iolist) :: :ok
+ def send({__MODULE__, pid, _}, line) do
+ GenServer.call(pid, {:send, line})
+ end
+
+ @doc false
+ def init([caller, tls, host, port, options]) do
+ Process.monitor(caller)
+ module = if tls, do: :ssl, else: :gen_tcp
+ state = %{caller: caller, module: module, host: host, port: port, options: options, socket: nil}
+ case module.connect(String.to_charlist(host), port, options) do
+ {:ok, socket} ->
+ {:ok, Map.put(state, :socket, socket)}
+ error ->
+ {:error, error}
+ end
+ end
+
+ @doc false
+ def handle_call({:send, data}, {caller, _}, state = %{caller: caller, module: module, socket: socket}) do
+ {:reply, module.send(socket, data), state}
+ end
+
+ def handle_call(:close, {caller, _}, state = %{caller: caller, module: module, socket: socket}) do
+ {:stop, :normal, module.close(socket), state = %{state | module: nil, socket: nil}}
+ end
+
+ def handle_call(_, {caller, _}, state = %{caller: caller}) do
+ {:reply, {:error, :invalid_call}, state}
+ end
+
+ def handle_call(_, _, state) do
+ {:noreply, state}
+ end
+
+ @doc false
+ # Received a line from socket
+ def handle_info({module, socket, line}, state = %{caller: caller, module: module, socket: socket}) do
+ Kernel.send(caller, {__MODULE__, self(), Line.parse(line)})
+ {:noreply, state}
+ end
+
+ # Socket is closed
+ def handle_info({closed, socket}, state = %{socket: socket, caller: caller}) when closed in [:tcp_closed, :ssl_closed] do
+ Kernel.send(caller, {__MODULE__, self(), :closed})
+ {:stop, :normal, state}
+ end
+
+ # Socket error
+ # TODO: SSL errors
+ def handle_info({:tcp_error, socket, reason}, state = %{socket: socket, caller: caller}) do
+ Kernel.send(caller, {__MODULE__, self(), {:error, reason}})
+ {:stop, :normal, state}
+ end
+
+ # Caller is down
+ def handle_info({:DOWN, _, :process, caller, reason}, state = %{caller: caller, module: module, socket: socket}) do
+ module.close(socket)
+ {:stop, :normal, state}
+ end
+
+ def terminate(reason, state) do
+ state
+ end
+
+end
diff --git a/lib/irc/parser/capabs.ex b/lib/irc/parser/capabs.ex
new file mode 100644
index 0000000..88bf017
--- /dev/null
+++ b/lib/irc/parser/capabs.ex
@@ -0,0 +1,32 @@
+defmodule Irc.Parser.Capabs do
+ @moduledoc "Helper to parse capability lists"
+
+ @spec parse(String.t) :: %{capab :: String.t => true | args :: Map.t}
+ def parse(string) do
+ string
+ |> String.split(" ")
+ |> Enum.map(&parse_capab/1)
+ |> Enum.into(Map.new)
+ end
+
+ defp parse_capab(string) do
+ case String.split(string, "=", parts: 2) do
+ [capab] ->
+ {capab, true}
+ [capab, args] ->
+ parse_capab_with_args(capab, args)
+ end
+ end
+
+ defp parse_capab_with_args(capab, args) do
+ args = args
+ |> String.split(",")
+ |> Map.reduce(%{}, fn(k_v, acc) ->
+ [k, v] = String.split(k_v, "=")
+ Map.put(acc, k, v)
+ end)
+ {capab, args}
+ end
+
+end
+
diff --git a/lib/irc/parser/line.ex b/lib/irc/parser/line.ex
index 17c381c..5aa5a9b 100644
--- a/lib/irc/parser/line.ex
+++ b/lib/irc/parser/line.ex
@@ -37,13 +37,25 @@ defmodule Irc.Parser.Line do
|> Enum.join(" ")
end
- def new(command, args \\ [], tags \\ %{}) do
+ def new(command) do
+ new(command, [])
+ end
+
+ def new(command, args) do
+ new(command, args, %{})
+ end
+
+ def new(command, arg, tags) when not is_list(arg) do
+ new(command, [arg], tags)
+ end
+
+ def new(command, args, tags) do
%__MODULE__{command: command, args: args, tags: tags, __private__: %{at: DateTime.utc_now()}}
end
@doc "Returns the line date (server time if sent, otherwise, parse time)"
@spec at(t()) :: DateTime.t()
- def at(%__MODULE__{__private__: %{at: at}, tags: %{"time" => server_time}}) do
+ def at(line = %__MODULE__{__private__: %{at: at}, tags: %{"time" => server_time}}) do
case DateTime.from_iso8601(server_time) do
{:ok, date} -> date
_ -> at
@@ -54,14 +66,14 @@ defmodule Irc.Parser.Line do
@spec to?(t(), Irc.Connection.t() | Irc.Mask.t() | Irc.User.t()) :: boolean
@doc "Returns true if the line is adressed to the connection."
- def to?(%__MODULE__{args: [nick | _]}, %{__struct__: s, nick: nick}) when s in [Irc.Connection, Irc.Mask, Irc.User] do
+ def to?(line = %__MODULE__{args: [nick | _]}, target = %{__struct__: s, nick: nick}) when s in [Irc.Connection, Irc.Mask, Irc.User] do
true
end
def to?(%__MODULE__{}, %{__struct__: s}) when s in [Irc.Connection, Irc.Mask, Irc.User], do: false
@spec self?(t(), Irc.Connection.t() | Irc.Mask.t() | Irc.User.t()) :: boolean
@doc "Returns true if the line source is the from the given connection/mask."
- def self?(%__MODULE__{source: %Irc.Mask{nick: nick}}, %Irc.Connection{nick: nick}) do
+ def self?(line = %__MODULE__{source: %Irc.Mask{nick: nick}}, target = %Irc.Connection{nick: nick}) do
true
end
def self?(%__MODULE__{source: mask = %Irc.Mask{}}, mask = %Irc.Mask{}) do
diff --git a/lib/irc/sts.ex b/lib/irc/sts.ex
index 9980f8c..b71d032 100644
--- a/lib/irc/sts.ex
+++ b/lib/irc/sts.ex
@@ -1,29 +1,45 @@
defmodule Irc.STS do
+ require Logger
+ use GenServer
@moduledoc """
# STS Store.
When a connection encounters a STS policy, it signals it using `witness/4`. The store will consider the policy valid indefinitely as long as
- any connection are still alive for that host/port pair. Once all connections or the system stops, the latest policy expiration date will be computed.
+ any connection are still alive for that host.
- By default, the store is not persistent. If you wish to enable persistance, set the `:irc, :sts_cache_file` app environment.
+ Once all connections or the system stops, the latest policy expiration date will be computed.
+
+ The store is shared for all a given node/uses of the IRC app, but can be disabled per connection basis, by disabling STS.
+
+ By default, the store is persisted to disk. If you wish to configure persistance, set the `:irc, :sts_store_file` app environment:
+
+ * `nil` to disable,
+ * `{:priv, "filename.dets"}` to store in the irc app priv directory,
+ * `{:priv, app, "filename.dets"}` to store in another app priv directory,
+ * any file path as string.
"""
@ets __MODULE__.ETS
- # tuple {{host,port}, tls_port, period, until | true, at}
+ # tuple {host, # hostname
+ # tls_port, # port to use
+ # period, # advertised period. use `until` instead
+ # until | true, # until which date the entry is valid. true if connections are already connected with the sts policy.
+ # at # last witness time
+ # }
@doc "Lookup a STS entry"
- @spec lookup(host :: String.t(), port :: Integer.t()) :: {enabled :: boolean, port :: Integer.t()}
- def lookup(host,port) do
+ @spec lookup(host :: String.t()) :: {:ok, port :: Integer.t()} | nil
+ def lookup(host) do
with \
- [{_, port, period, until}] <- :ets.lookup(@ets, {host,port}),
+ [{_, port, period, until, _}] <- :ets.lookup(@ets, host),
true <- verify_validity(period, until)
do
- {true, port}
+ {:ok, port}
else
- [] -> {false, port}
+ [] -> nil
false ->
- GenServer.cast(__MODULE__, {:expired, {host,port}})
- {false, port}
+ GenServer.cast(__MODULE__, {:expired, host})
+ nil
end
end
@@ -33,33 +49,42 @@ defmodule Irc.STS do
The STS cache will consider the policy as infinitely valid as long as the calling PID is alive,
or signal a new `witness/4`.
"""
- def witness(host, port, sts_port, period) do
- GenServer.call(__MODULE__, {:witness, host, port, sts_port, period, self()})
- end
-
- @doc """
- Revoke a STS policy. This is the same as calling `witness/4` with sts_port = nil and period = nil.
- """
- def revoke(host, port) do
- GenServer.call(__MODULE__, {:witness, {host,port,nil,nil,self()}})
+ def witness(host, policy) do
+ GenServer.call(__MODULE__, {:witness, host, policy, self()})
end
@doc "Returns all entries in the STS store"
def all() do
- fold = fn(el, acc) -> [el | acc] end
- :ets.foldl(fold, @ets, [])
+ fold = fn({host, port, period, until, at}, acc) ->
+ policy = %{port: port, period: period, until: until, at: at}
+ Map.put(acc, host, policy)
+ end
+ :ets.foldl(fold, @ets, Map.new)
end
- def start_link() do
- GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ def start_link(args) do
+ GenServer.start_link(__MODULE__, args, [name: __MODULE__])
end
def init(_) do
ets = :ets.new(@ets, [:named_table, :protected])
- cache_file = Application.get_env(:irc, :sts_cache_file)
- dets = if cache_file do
- {:ok, dets} = :dets.open_file(cache_file)
+ store_file = parse_env_store_file(Application.get_env(:irc, :sts_store_file, {:priv, "sts_policies.dets"}))
+ dets = if store_file do
+ {:ok, dets} = :dets.open_file(store_file, [])
true = :ets.from_dets(ets, dets)
+ # Fix possible stale entries by using their last witness known time
+ fold = fn
+ ({key, _tls_port, period, true, at}, acc) ->
+ until = at |> DateTime.add(period)
+ [{key, until} | acc]
+ (_, acc) -> acc
+ end
+ for {key, until} <- :ets.foldl(fold, [], @ets) do
+ :ets.update_element(@ets, key, {4, until})
+ end
+ :ets.to_dets(@ets, dets)
+ :dets.sync(dets)
+ dets
end
{:ok, %{ets: ets, dets: dets, map: %{}}}
end
@@ -82,25 +107,25 @@ defmodule Irc.STS do
end
# Duration 0 -- Policy is removed
- def handle_call({:witness, host, port, _tls_port, period, pid}, from, state) when period in ["0", 0, nil] do
- state = remove({host,port}, state)
- {:reply, :ok, state, {:handle_continue, {:remove, {host,port}}}}
+ def handle_call({:witness, host, %{"duration" => period}, pid}, from, state) when period in ["0", 0, nil] do
+ state = remove(host, state)
+ {:reply, :ok, state, {:handle_continue, {:remove, host}}}
end
# Witnessed policy.
# As long as caller PID is alive, consider the policy always valid
- def handle_call({:witness, host, port, tls_port, period, pid}, _, state) do
- entry = {{host,port}, tls_port, period, true}
+ def handle_call({:witness, host, %{"port" => tls_port, "duration" => duration}, pid}, _, state) do
+ entry = {host, tls_port, duration, true, DateTime.utc_now()}
:ets.insert(@ets, entry)
mon = Process.monitor(pid)
- state = %{state | map: Map.put(state.map, pid, {mon,{host,port}})}
+ state = %{state | map: Map.put(state.map, pid, {mon,host})}
{:reply, :ok, state, {:handle_continue, {:write, entry}}}
end
# Caller side encountered an expired policy, check and remove it.
def handle_cast({:expired, key}, state) do
{state, continue} = case :ets.lookup(@ets, key) do
- [{_, _, period, until}] ->
+ [{_, _, period, until, _at}] ->
if !verify_validity(period, until) do
{remove(key, state), {:remove, key}}
else
@@ -117,13 +142,18 @@ defmodule Irc.STS do
others = Enum.filter(state.map, fn({p, {_,k}}) -> k == key && p != pid end)
state = %{state | map: Map.delete(state.map, pid)}
if key && Enum.empty?(others) do
- case :ets.lookup(@ets, key) do
- [{key, tls_port, period, until}] ->
- until = DateTime.utc_now() |> DateTime.add(period)
- entry = {key, tls_port, period, until}
+ case {Enum.empty?(others), :ets.lookup(@ets, key)} do
+ {last?, [{key, tls_port, period, until, at}]} ->
+ now = DateTime.utc_now()
+ until = if last? do
+ DateTime.add(now, period)
+ else
+ true
+ end
+ entry = {key, tls_port, period, until, now}
:ets.insert(@ets, entry)
{:noreply, state, {:handle_continue, {:write, entry}}}
- [] ->
+ {_, []} ->
{:noreply, state}
end
else
@@ -135,7 +165,7 @@ defmodule Irc.STS do
def terminate(_, state) do
if state.dets do
fold = fn
- ({key, _tls_port, period, true}, acc) ->
+ ({key, _tls_port, period, true, _}, acc) ->
until = DateTime.utc_now() |> DateTime.add(period)
[{key, until} | acc]
(_, acc) -> acc
@@ -166,4 +196,26 @@ defmodule Irc.STS do
DateTime.utc_now() >= until
end
+ defp parse_env_store_file({:priv, file}) do
+ parse_env_store_file({:priv, :irc, file})
+ end
+
+ defp parse_env_store_file({:priv, app, file}) do
+ :code.priv_dir(app) ++ '/' ++ String.to_charlist(file)
+ end
+
+ defp parse_env_store_file(string) when is_binary(string) do
+ String.to_charlist(string)
+ end
+
+ defp parse_env_store_file(nil) do
+ Logger.info "Irc.STS: Permanent cache NOT ENABLED"
+ nil
+ end
+
+ defp parse_env_store_file(_invalid) do
+ Logger.error "Irc.STS: Invalid cache file configuration, permanent store NOT ENABLED"
+ nil
+ end
+
end