diff --git a/lib/farside/application.ex b/lib/farside/application.ex index 708ff0d..fd199e1 100644 --- a/lib/farside/application.ex +++ b/lib/farside/application.ex @@ -8,7 +8,8 @@ defmodule Farside.Application do def start(_type, _args) do children = [ Plug.Cowboy.child_spec(scheme: :http, plug: Farside.Router, options: [port: 4001]), - {Redix, {@redis_conn, [name: :redix]}} + {Redix, {@redis_conn, [name: :redix]}}, + {PlugAttack.Storage.Ets, name: Farside.Throttle.Storage, clean_period: 60_000} ] opts = [strategy: :one_for_one, name: Farside.Supervisor] diff --git a/lib/farside/router.ex b/lib/farside/router.ex index 7f31e04..e2014b2 100644 --- a/lib/farside/router.ex +++ b/lib/farside/router.ex @@ -3,6 +3,7 @@ defmodule Farside.Router do use Plug.Router + plug(Farside.Throttle) plug(:match) plug(:dispatch) diff --git a/lib/farside/throttle.ex b/lib/farside/throttle.ex new file mode 100644 index 0000000..fc8b591 --- /dev/null +++ b/lib/farside/throttle.ex @@ -0,0 +1,19 @@ +defmodule Farside.Throttle do + import Plug.Conn + use PlugAttack + + rule "throttle per ip", conn do + # throttle to 1 request per second + throttle conn.remote_ip, + period: 1_000, limit: 1, + storage: {PlugAttack.Storage.Ets, Farside.Throttle.Storage} + end + + def allow_action(conn, _data, _opts), do: conn + + def block_action(conn, _data, _opts) do + conn + |> send_resp(:forbidden, "Exceeded rate limit\n") + |> halt + end +end diff --git a/mix.exs b/mix.exs index fbbbcc4..f6f91cd 100644 --- a/mix.exs +++ b/mix.exs @@ -22,11 +22,12 @@ defmodule Farside.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ + {:httpoison, "~> 1.8"}, {:jason, "~> 1.1"}, + {:plug_attack, "~> 0.4.2"}, {:plug_cowboy, "~> 2.0"}, {:poison, "~> 5.0"}, - {:httpoison, "~> 1.8"}, - {:redix, "~> 1.1"} + {:redix, "~> 1.1"}, ] end end diff --git a/mix.lock b/mix.lock index bb98ecc..26a5cc7 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,7 @@ "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"}, "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, + "plug_attack": {:hex, :plug_attack, "0.4.3", "88e6c464d68b1491aa083a0347d59d58ba71a7e591a7f8e1b675e8c7792a0ba8", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9ed6fb8a6f613a36040f2875130a21187126c5625092f24bc851f7f12a8cbdc1"}, "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, diff --git a/test/farside_test.exs b/test/farside_test.exs index 786c382..b5561e0 100644 --- a/test/farside_test.exs +++ b/test/farside_test.exs @@ -8,22 +8,21 @@ defmodule FarsideTest do @opts Router.init([]) - test "/" do - conn = - :get - |> conn("/", "") + def test_conn(path) do + :timer.sleep(1000) + :get + |> conn(path, "") |> Router.call(@opts) + end + test "/" do + conn = test_conn("/") assert conn.state == :sent assert conn.status == 200 end test "/ping" do - conn = - :get - |> conn("/ping", "") - |> Router.call(@opts) - + conn = test_conn("/ping") assert conn.state == :sent assert conn.status == 200 assert conn.resp_body == "PONG" @@ -42,24 +41,16 @@ defmodule FarsideTest do IO.puts("") Enum.map(service_names, fn service_name -> - - conn = - :get - |> conn("/#{service_name}", "") - |> Router.call(@opts) - + conn = test_conn("/#{service_name}") first_redirect = elem(List.last(conn.resp_headers), 1) + IO.puts(" /#{service_name} (#1) -- #{first_redirect}") assert conn.state == :set assert conn.status == 302 - - conn = - :get - |> conn("/#{service_name}", "") - |> Router.call(@opts) - + conn = test_conn("/#{service_name}") second_redirect = elem(List.last(conn.resp_headers), 1) + IO.puts(" /#{service_name} (#2) -- #{second_redirect}") assert conn.state == :set assert conn.status == 302