diff --git a/.iex.exs b/.iex.exs index 2bbf4dd0..e8c7de4a 100644 --- a/.iex.exs +++ b/.iex.exs @@ -1,2 +1,3 @@ alias Philomena.{Repo, Comments.Comment, Posts.Post, Images.Image, Tags.Tag, Users.User} -import Ecto.Query \ No newline at end of file +import Ecto.Query +import Ecto.Changeset \ No newline at end of file diff --git a/assets/static/images/captcha/1.png b/assets/static/images/captcha/1.png new file mode 100644 index 00000000..98493c22 Binary files /dev/null and b/assets/static/images/captcha/1.png differ diff --git a/assets/static/images/captcha/2.png b/assets/static/images/captcha/2.png new file mode 100644 index 00000000..c6944f4d Binary files /dev/null and b/assets/static/images/captcha/2.png differ diff --git a/assets/static/images/captcha/3.png b/assets/static/images/captcha/3.png new file mode 100644 index 00000000..add09801 Binary files /dev/null and b/assets/static/images/captcha/3.png differ diff --git a/assets/static/images/captcha/4.png b/assets/static/images/captcha/4.png new file mode 100644 index 00000000..2a602ecf Binary files /dev/null and b/assets/static/images/captcha/4.png differ diff --git a/assets/static/images/captcha/5.png b/assets/static/images/captcha/5.png new file mode 100644 index 00000000..20fcbe85 Binary files /dev/null and b/assets/static/images/captcha/5.png differ diff --git a/assets/static/images/captcha/6.png b/assets/static/images/captcha/6.png new file mode 100644 index 00000000..6c96307d Binary files /dev/null and b/assets/static/images/captcha/6.png differ diff --git a/assets/static/images/captcha/background.png b/assets/static/images/captcha/background.png new file mode 100644 index 00000000..af1e9b26 Binary files /dev/null and b/assets/static/images/captcha/background.png differ diff --git a/assets/static/images/captcha/i1.png b/assets/static/images/captcha/i1.png new file mode 100644 index 00000000..44c64070 Binary files /dev/null and b/assets/static/images/captcha/i1.png differ diff --git a/assets/static/images/captcha/i2.png b/assets/static/images/captcha/i2.png new file mode 100644 index 00000000..03fe0d25 Binary files /dev/null and b/assets/static/images/captcha/i2.png differ diff --git a/assets/static/images/captcha/i3.png b/assets/static/images/captcha/i3.png new file mode 100644 index 00000000..43b1acc0 Binary files /dev/null and b/assets/static/images/captcha/i3.png differ diff --git a/assets/static/images/captcha/i4.png b/assets/static/images/captcha/i4.png new file mode 100644 index 00000000..436c7ee6 Binary files /dev/null and b/assets/static/images/captcha/i4.png differ diff --git a/assets/static/images/captcha/i5.png b/assets/static/images/captcha/i5.png new file mode 100644 index 00000000..ccbab027 Binary files /dev/null and b/assets/static/images/captcha/i5.png differ diff --git a/assets/static/images/captcha/i6.png b/assets/static/images/captcha/i6.png new file mode 100644 index 00000000..09f8e3a3 Binary files /dev/null and b/assets/static/images/captcha/i6.png differ diff --git a/lib/philomena/application.ex b/lib/philomena/application.ex index a78bf76a..e7e3c37c 100644 --- a/lib/philomena/application.ex +++ b/lib/philomena/application.ex @@ -14,7 +14,8 @@ defmodule Philomena.Application do PhilomenaWeb.Endpoint, # Starts a worker by calling: Philomena.Worker.start_link(arg) # {Philomena.Worker, arg}, - Pow.Store.Backend.MnesiaCache + Pow.Store.Backend.MnesiaCache, + {Redix, name: :redix} ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/philomena/captcha.ex b/lib/philomena/captcha.ex new file mode 100644 index 00000000..820bb10d --- /dev/null +++ b/lib/philomena/captcha.ex @@ -0,0 +1,130 @@ +defmodule Philomena.Captcha do + defstruct [:image_base64, :solution, :solution_id] + + @numbers ~W(1 2 3 4 5 6) + @images ~W(1 2 3 4 5 6) + @base_path File.cwd!() <> "/assets/static/images/captcha" + + @number_files %{ + "1" => @base_path <> "/1.png", + "2" => @base_path <> "/2.png", + "3" => @base_path <> "/3.png", + "4" => @base_path <> "/4.png", + "5" => @base_path <> "/5.png", + "6" => @base_path <> "/6.png" + } + + @image_files %{ + "1" => @base_path <> "/i1.png", + "2" => @base_path <> "/i2.png", + "3" => @base_path <> "/i3.png", + "4" => @base_path <> "/i4.png", + "5" => @base_path <> "/i5.png", + "6" => @base_path <> "/i6.png" + } + + @background_file @base_path <> "/background.png" + + @geometry %{ + 1 => "+0+0", 2 => "+120+0", 3 => "+240+0", + 4 => "+0+120", 5 => "+120+120", 6 => "+240+120", + 7 => "+0+240", 8 => "+120+240", 9 => "+240+240" + } + + @distortion_1 [ + ~W"-implode .1", + ~W"-implode -.1" + ] + + @distortion_2 [ + ~W"-swirl 10", + ~W"-swirl -10", + ~W"-swirl 20", + ~W"-swirl -20" + ] + + @distortion_3 [ + ~W"-wave 5x180", + ~W"-wave 5x126", + ~W"-wave 10x180", + ~W"-wave 10x126" + ] + + def create do + solution = + Enum.zip(@numbers, Enum.shuffle(@images)) + |> Map.new() + + # 3x3 render grid + grid = Enum.shuffle(@numbers ++ [nil, nil, nil]) + + # Base arguments + args = [ + "-page", "360x360", + @background_file + ] + + # Individual grid files + files = + grid + |> Enum.with_index() + |> Enum.flat_map(fn {num, index} -> + if num do + [ + "(", @image_files[solution[num]], ")", "-geometry", @geometry[index + 1], "-composite", + "(", @number_files[num], ")", "-geometry", @geometry[index + 1], "-composite" + ] + else + [] + end + end) + + # Distortions for more unpredictability + distortions = + [ + Enum.random(@distortion_1), + Enum.random(@distortion_2), + Enum.random(@distortion_3) + ] + |> Enum.shuffle() + |> List.flatten() + + jpeg = ~W"-quality 8 jpeg:-" + + {image, 0} = System.cmd("convert", args ++ files ++ distortions ++ jpeg) + image = image |> Base.encode64() + + # Store solution in redis to prevent reuse + # Solutions are valid for 10 minutes + solution_id = + :crypto.strong_rand_bytes(12) + |> Base.encode16(case: :lower) + solution_id = "cp_" <> solution_id + + {:ok, _ok} = Redix.command(:redix, ["SET", solution_id, Jason.encode!(solution)]) + {:ok, _ok} = Redix.command(:redix, ["EXPIRE", solution_id, 600]) + + %Philomena.Captcha{ + image_base64: image, + solution: solution, + solution_id: solution_id + } + end + + def valid_solution?(<<"cp_", _rest::binary>> = solution_id, solution) do + # Delete key immediately. This may race, but should + # have minimal impact if the race succeeds. + with {:ok, sol} <- Redix.command(:redix, ["GET", solution_id]), + {:ok, _del} <- Redix.command(:redix, ["DEL", solution_id]), + {:ok, sol} <- Jason.decode(to_string(sol)) + do + Map.equal?(solution, sol) + else + _ -> + false + end + end + + def valid_solution?(_solution_id, _solution), + do: false +end diff --git a/lib/philomena_web/controllers/captcha_controller.ex b/lib/philomena_web/controllers/captcha_controller.ex new file mode 100644 index 00000000..705936db --- /dev/null +++ b/lib/philomena_web/controllers/captcha_controller.ex @@ -0,0 +1,11 @@ +defmodule PhilomenaWeb.CaptchaController do + use PhilomenaWeb, :controller + + alias Philomena.Captcha + + def create(conn, _params) do + captcha = Captcha.create() + + render(conn, "create.html", captcha: captcha, layout: false) + end +end \ No newline at end of file diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index edf178e9..08a79109 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -67,6 +67,7 @@ defmodule PhilomenaWeb.Router do end resources "/filters", FilterController resources "/profiles", ProfileController, only: [:show] + resources "/captchas", CaptchaController, only: [:create] get "/:id", ImageController, :show end diff --git a/lib/philomena_web/templates/captcha/create.html.slime b/lib/philomena_web/templates/captcha/create.html.slime new file mode 100644 index 00000000..650cc52b --- /dev/null +++ b/lib/philomena_web/templates/captcha/create.html.slime @@ -0,0 +1,20 @@ +elixir: + options = [ + "Applejack": 1, + "Fluttershy": 2, + "Pinkie Pie": 3, + "Rainbow Dash": 4, + "Rarity": 5, + "Twilight Sparkle": 6 + ] + +div + = hidden_input :captcha, :id, value: @captcha.solution_id + img src="data:image/jpeg;base64,#{@captcha.image_base64}" + + = for i <- (1..6) do + .field + label> for="captcha_sln[#{i}]" + | Name of pony with cutie mark # + = i + = select :captcha, "sln[#{i}]", options, class: "input" diff --git a/lib/philomena_web/templates/pow/session/new.html.slime b/lib/philomena_web/templates/pow/session/new.html.slime index 24b51147..4b65a30d 100644 --- a/lib/philomena_web/templates/pow/session/new.html.slime +++ b/lib/philomena_web/templates/pow/session/new.html.slime @@ -19,6 +19,10 @@ h1 Sign in = checkbox f, :persistent_session = label f, :persistent_session, "Remember me" + .field + = checkbox f, :captcha, class: "js-captcha", value: 0 + = label f, :captcha, "I am not a robot!" + = submit "Sign in", class: "button" p diff --git a/lib/philomena_web/views/captcha_view.ex b/lib/philomena_web/views/captcha_view.ex new file mode 100644 index 00000000..12dc3e7e --- /dev/null +++ b/lib/philomena_web/views/captcha_view.ex @@ -0,0 +1,3 @@ +defmodule PhilomenaWeb.CaptchaView do + use PhilomenaWeb, :view +end diff --git a/mix.exs b/mix.exs index 8db1d8c8..32c72351 100644 --- a/mix.exs +++ b/mix.exs @@ -54,7 +54,8 @@ defmodule Philomena.MixProject do {:canary, "~> 1.1.1"}, {:scrivener_ecto, "~> 2.0"}, {:pbkdf2, "~> 2.0"}, - {:qrcode, "~> 0.1.5"} + {:qrcode, "~> 0.1.5"}, + {:redix, "~> 0.10.2"} ] end diff --git a/mix.lock b/mix.lock index cefdf126..59dac626 100644 --- a/mix.lock +++ b/mix.lock @@ -43,6 +43,7 @@ "pow": {:hex, :pow, "1.0.13", "5ca3e8d9fecca037bfb0ea3b8dde070cc319746498e844d59fc209d461b0d426", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3.0 or ~> 1.4.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 2.0.0 and <= 3.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, "qrcode": {:hex, :qrcode, "0.1.5", "551271830515c150f34568345b060c625deb0e6691db2a01b0a6de3aafc93886", [:mix], [], "hexpm"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, + "redix": {:hex, :redix, "0.10.2", "a9eabf47898aa878650df36194aeb63966d74f5bd69d9caa37babb32dbb93c5d", [:mix], [{:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "retry": {:hex, :retry, "0.13.0", "bb9b2713f70f39337837852337ad280c77662574f4fb852a8386c269f3d734c4", [:mix], [], "hexpm"}, "scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"}, "scrivener_ecto": {:hex, :scrivener_ecto, "2.2.0", "53d5f1ba28f35f17891cf526ee102f8f225b7024d1cdaf8984875467158c9c5e", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm"},