summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Busby <noreply+git@benbusby.com>2021-11-12 14:34:36 -0700
committerBen Busby <noreply+git@benbusby.com>2021-11-12 14:34:36 -0700
commit2d988a1239bd9988d453df5baba75a610f979eb3 (patch)
tree874e3f93d33e9c098ed9c0eae85721a8878c6210
parent8ee4f308a47ea613a97493b8afbe1aed02f80036 (diff)
downloadfarside-2d988a1239bd9988d453df5baba75a610f979eb3.tar.gz
Throttle incoming requests to 1/sec per ip
This introduces a way of throttling requests in a way that makes sense for the purpose of the app. The app only supports redirecting to one particular service when browsing, which would seldom be required more than once per second for normal "human" browsing. Without this, the service could easily be used to DOS multiple instances at once. That being said, anyone concerned about someone DOS-ing multiple instances at once should be aware that this would be trivial to do with a simple bash script. This is simply a preventative measure to hopefully deter people from trying to attack all public instances of private frontends using farside.link. Note that this throttling applies to all routes in the app, including the homepage. This could be updated to exclude the homepage I guess, but I'm not really sure what the use case would be for that.
-rw-r--r--lib/farside/application.ex3
-rw-r--r--lib/farside/router.ex1
-rw-r--r--lib/farside/throttle.ex19
-rw-r--r--mix.exs5
-rw-r--r--mix.lock1
-rw-r--r--test/farside_test.exs33
6 files changed, 38 insertions, 24 deletions
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