| defmodule Plausible.Session.Salts do |
| use GenServer |
| use Plausible.Repo |
|
|
| def start_link(opts) do |
| GenServer.start_link(__MODULE__, opts, name: opts[:name] || __MODULE__) |
| end |
|
|
| @impl true |
| def init(opts) do |
| name = opts[:name] || __MODULE__ |
| now = opts[:now] || DateTime.utc_now() |
| clean_old_salts(now) |
|
|
| ^name = |
| :ets.new(name, [ |
| :named_table, |
| :set, |
| :protected, |
| {:read_concurrency, true} |
| ]) |
|
|
| refresh(name, now) |
|
|
| {:ok, name} |
| end |
|
|
| def refresh(name, now) do |
| salts = try do |
| Repo.all(from s in "salts", select: s.salt, order_by: [desc: s.id], limit: 2) |
| rescue |
| _ -> [] |
| end |
|
|
| state = |
| case salts do |
| [current, prev] -> |
| %{previous: prev, current: current} |
|
|
| [current] -> |
| %{previous: nil, current: current} |
|
|
| [] -> |
| new = generate_and_persist_new_salt(now) |
| %{previous: nil, current: new} |
| end |
|
|
| true = :ets.insert(name, {:state, state}) |
| interval = Application.get_env(:plausible, __MODULE__)[:interval] || :timer.seconds(90) |
| Process.send_after(self(), {:refresh, now}, interval) |
| :ok |
| end |
|
|
| def rotate(name \\ __MODULE__, now \\ DateTime.utc_now()) do |
| GenServer.call(name, {:rotate, now}) |
| end |
|
|
| def fetch(name \\ __MODULE__) do |
| [state: state] = :ets.lookup(name, :state) |
| state |
| end |
|
|
| @impl true |
| def handle_call({:rotate, now}, _from, name) do |
| current = fetch(name).current |
| clean_old_salts(now) |
|
|
| state = |
| %{ |
| current: generate_and_persist_new_salt(now), |
| previous: current |
| } |
|
|
| true = :ets.insert(name, {:state, state}) |
| {:reply, :ok, name} |
| end |
|
|
| @impl true |
| def handle_info({:refresh, now}, name) do |
| refresh(name, now) |
| {:noreply, name} |
| end |
|
|
| defp generate_and_persist_new_salt(now) do |
| salt = :crypto.strong_rand_bytes(16) |
| try do |
| Repo.insert_all("salts", [%{salt: salt, inserted_at: now}]) |
| rescue |
| _ -> :ok |
| end |
| salt |
| end |
|
|
| defp clean_old_salts(now) do |
| h48_ago = |
| DateTime.shift(now, hour: -48) |
|
|
| try do |
| Repo.delete_all(from s in "salts", where: s.inserted_at < ^h48_ago) |
| rescue |
| _ -> :ok |
| end |
| end |
| end |
|
|