From 66dd5d5b9a2f768a12a39d33f89f337a0788d88f Mon Sep 17 00:00:00 2001 From: Mitchell Gerber Date: Mon, 22 Jan 2018 22:03:22 -0600 Subject: [PATCH] server/client - rate limiting for threads/replies --- client/app/components/editor/editor.tsx | 10 ++++++++-- lib/myapp/rate_limiter.ex | 18 ++++++++++++++++++ lib/myapp_web/controllers/reply_controller.ex | 13 +++++++++---- lib/myapp_web/controllers/thread_controller.ex | 13 +++++++++---- 4 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 lib/myapp/rate_limiter.ex diff --git a/client/app/components/editor/editor.tsx b/client/app/components/editor/editor.tsx index 8e09dbc..db554fe 100644 --- a/client/app/components/editor/editor.tsx +++ b/client/app/components/editor/editor.tsx @@ -80,6 +80,12 @@ export class Editor extends React.Component { } } + getErrorMessage(e: any) { + return get(e, 'response.status') === 429 ? + 'You are doing that too much! Please try again in a few minutes.' : + 'Server error. Please try again later.'; + } + async newReply() { const { content } = this.state; @@ -99,7 +105,7 @@ export class Editor extends React.Component { this.props.editingReply ? await axios.put('/api/reply', data) : await axios.post('/api/reply', data); this.props.onClose(false); } catch (e) { - this.setState({ errorMessage: 'Server error. Please try again later.' }); + this.setState({ errorMessage: this.getErrorMessage(e) }); } } @@ -121,7 +127,7 @@ export class Editor extends React.Component { await axios.post('/api/thread', data); this.props.onClose(false); } catch (e) { - this.setState({ errorMessage: 'Server error. Please try again later.' }); + this.setState({ errorMessage: this.getErrorMessage(e) }); } } diff --git a/lib/myapp/rate_limiter.ex b/lib/myapp/rate_limiter.ex new file mode 100644 index 0000000..c97623b --- /dev/null +++ b/lib/myapp/rate_limiter.ex @@ -0,0 +1,18 @@ +defmodule MyApp.RateLimiter do + + # map keys to integers to save memory + def new_reply_key, do: 1 + def new_thread_key, do: 2 + + @spec limit(String.t, integer, integer) :: {:ok, String.t} | {:error, String.t} + def limit(end_point, user_id, seconds) do + key = "rl#{end_point}:#{user_id}" + case Cachex.get(:myapp, key) do + {:missing, _} -> + Cachex.set(:myapp, key, true, ttl: :timer.seconds(seconds)) + {:ok, "ok"} + {:ok, _} -> {:error, "limit reached"} + end + end + +end diff --git a/lib/myapp_web/controllers/reply_controller.ex b/lib/myapp_web/controllers/reply_controller.ex index 4753abc..86b1e7b 100644 --- a/lib/myapp_web/controllers/reply_controller.ex +++ b/lib/myapp_web/controllers/reply_controller.ex @@ -2,6 +2,7 @@ defmodule MyAppWeb.ReplyController do use MyAppWeb, :controller alias MyAppWeb.Response alias MyApp.Data + alias MyApp.RateLimiter @spec insert(map, map) :: any def insert(conn, params) do @@ -9,10 +10,14 @@ defmodule MyAppWeb.ReplyController do |> MyApp.Guardian.Plug.current_claims |> Map.get("id") - {output, status} = params - |> Map.put("user_id", user_id) - |> Data.Reply.insert - |> Response.put_resp + {output, status} = case RateLimiter.limit(RateLimiter.new_reply_key, user_id, 60) do + {:ok, _} -> params + |> Map.put("user_id", user_id) + |> Data.Reply.insert + |> Response.put_resp + + {:error, error} -> {error, 429} + end conn |> put_status(status) diff --git a/lib/myapp_web/controllers/thread_controller.ex b/lib/myapp_web/controllers/thread_controller.ex index fd74616..076bd22 100644 --- a/lib/myapp_web/controllers/thread_controller.ex +++ b/lib/myapp_web/controllers/thread_controller.ex @@ -2,6 +2,7 @@ defmodule MyAppWeb.ThreadController do use MyAppWeb, :controller alias MyAppWeb.Response alias MyApp.Data + alias MyApp.RateLimiter @spec insert(map, map) :: any def insert(conn, params) do @@ -9,10 +10,14 @@ defmodule MyAppWeb.ThreadController do |> MyApp.Guardian.Plug.current_claims |> Map.get("id") - {output, status} = params - |> Map.put("user_id", user_id) - |> Data.Thread.insert - |> Response.put_resp + # every 5 minutes user can submit new thread + {output, status} = case RateLimiter.limit(RateLimiter.new_thread_key, user_id, 300) do + {:ok, _} -> params + |> Map.put("user_id", user_id) + |> Data.Thread.insert + |> Response.put_resp + {:error, error} -> {error, 429} + end conn |> put_status(status)