2019-11-11 13:13:06 +01:00
# Pleroma: A lightweight social networking server
2020-03-02 06:08:45 +01:00
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
2019-11-11 13:13:06 +01:00
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.RateLimiter do
@moduledoc """
## Configuration
2020-02-27 16:46:05 +01:00
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration .
The basic configuration is a tuple where :
2019-11-11 13:13:06 +01:00
* The first element : ` scale ` ( Integer ) . The time scale in milliseconds .
* The second element : ` limit ` ( Integer ) . How many requests to limit in the time scale provided .
2020-02-27 16:46:05 +01:00
It is also possible to have different limits for unauthenticated and authenticated users : the keyword value must be a
list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated .
2019-11-11 13:13:06 +01:00
To disable a limiter set its value to ` nil ` .
### Example
config :pleroma , :rate_limit ,
one : { 1000 , 10 } ,
two : [ { 10_000 , 10 } , { 10_000 , 50 } ] ,
foobar : nil
Here we have three limiters :
* ` one ` which is not over 10 req / 1 s
* ` two ` which has two limits : 10 req / 10 s for unauthenticated users and 50 req / 10 s for authenticated users
* ` foobar ` which is disabled
## Usage
AllowedSyntax :
plug ( Pleroma.Plugs.RateLimiter , name : :limiter_name )
plug ( Pleroma.Plugs.RateLimiter , options ) # :name is a required option
Allowed options :
* ` name ` required , always used to fetch the limit values from the config
* ` bucket_name ` overrides name for counting purposes ( e . g . to have a separate limit for a set of actions )
* ` params ` appends values of specified request params ( e . g . [ " id " ] ) to bucket name
Inside a controller :
plug ( Pleroma.Plugs.RateLimiter , [ name : :one ] when action == :one )
plug ( Pleroma.Plugs.RateLimiter , [ name : :two ] when action in [ :two , :three ] )
plug (
Pleroma.Plugs.RateLimiter ,
[ name : :status_id_action , bucket_name : " status_id_action:fav_unfav " , params : [ " id " ] ]
when action in ~w( fav_status unfav_status )a
)
or inside a router pipeline :
pipeline :api do
...
plug ( Pleroma.Plugs.RateLimiter , name : :one )
...
end
"""
import Pleroma.Web.TranslationHelpers
import Plug.Conn
2020-02-27 16:46:05 +01:00
alias Pleroma.Config
2019-11-11 13:13:06 +01:00
alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
alias Pleroma.User
2019-12-13 17:00:26 +01:00
require Logger
2020-02-27 16:46:05 +01:00
@doc false
def init ( plug_opts ) do
plug_opts
end
2019-11-11 13:13:06 +01:00
2020-02-27 16:46:05 +01:00
def call ( conn , plug_opts ) do
2020-03-13 19:15:42 +01:00
if disabled? ( conn ) do
2020-02-27 16:46:05 +01:00
handle_disabled ( conn )
else
action_settings = action_settings ( plug_opts )
handle ( conn , action_settings )
2019-11-11 13:13:06 +01:00
end
end
2020-02-27 16:46:05 +01:00
defp handle_disabled ( conn ) do
2020-03-13 19:15:42 +01:00
Logger . warn (
" Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter. "
)
2020-02-27 16:46:05 +01:00
conn
end
2019-11-11 13:13:06 +01:00
2020-02-27 16:46:05 +01:00
defp handle ( conn , nil ) , do : conn
2019-12-13 17:00:26 +01:00
2020-02-27 16:46:05 +01:00
defp handle ( conn , action_settings ) do
action_settings
|> incorporate_conn_info ( conn )
|> check_rate ( )
|> case do
{ :ok , _count } ->
2019-11-11 13:13:06 +01:00
conn
2020-02-27 16:46:05 +01:00
{ :error , _count } ->
render_throttled_error ( conn )
2019-11-11 13:13:06 +01:00
end
end
2020-03-13 19:15:42 +01:00
def disabled? ( conn ) do
2020-04-15 14:27:34 +02:00
if Map . has_key? ( conn . assigns , :remote_ip_found ) ,
do : ! conn . assigns . remote_ip_found ,
else : false
2019-12-13 17:00:26 +01:00
end
2020-02-29 20:04:09 +01:00
@inspect_bucket_not_found { :error , :not_found }
2020-02-27 16:46:05 +01:00
def inspect_bucket ( conn , bucket_name_root , plug_opts ) do
with %{ name : _ } = action_settings <- action_settings ( plug_opts ) do
action_settings = incorporate_conn_info ( action_settings , conn )
bucket_name = make_bucket_name ( %{ action_settings | name : bucket_name_root } )
key_name = make_key_name ( action_settings )
limit = get_limits ( action_settings )
2019-11-11 13:13:06 +01:00
2020-02-27 16:46:05 +01:00
case Cachex . get ( bucket_name , key_name ) do
{ :error , :no_cache } ->
2020-02-29 20:04:09 +01:00
@inspect_bucket_not_found
2019-11-11 13:13:06 +01:00
2020-02-27 16:46:05 +01:00
{ :ok , nil } ->
{ 0 , limit }
{ :ok , value } ->
{ value , limit - value }
end
else
2020-02-29 20:04:09 +01:00
_ -> @inspect_bucket_not_found
2020-02-27 16:46:05 +01:00
end
end
2019-11-11 13:13:06 +01:00
2020-02-27 16:46:05 +01:00
def action_settings ( plug_opts ) do
2020-02-28 14:33:42 +01:00
with limiter_name when is_atom ( limiter_name ) <- plug_opts [ :name ] ,
2020-02-27 16:46:05 +01:00
limits when not is_nil ( limits ) <- Config . get ( [ :rate_limit , limiter_name ] ) do
bucket_name_root = Keyword . get ( plug_opts , :bucket_name , limiter_name )
2019-11-11 13:13:06 +01:00
2020-02-27 16:46:05 +01:00
%{
name : bucket_name_root ,
limits : limits ,
opts : plug_opts
}
2019-11-11 13:13:06 +01:00
end
end
2020-02-27 16:46:05 +01:00
defp check_rate ( action_settings ) do
bucket_name = make_bucket_name ( action_settings )
key_name = make_key_name ( action_settings )
limit = get_limits ( action_settings )
2019-11-11 13:13:06 +01:00
case Cachex . get_and_update ( bucket_name , key_name , & increment_value ( &1 , limit ) ) do
{ :commit , value } ->
{ :ok , value }
{ :ignore , value } ->
{ :error , value }
{ :error , :no_cache } ->
2020-02-28 15:35:01 +01:00
initialize_buckets! ( action_settings )
2020-02-27 16:46:05 +01:00
check_rate ( action_settings )
2019-11-11 13:13:06 +01:00
end
end
defp increment_value ( nil , _limit ) , do : { :commit , 1 }
defp increment_value ( val , limit ) when val >= limit , do : { :ignore , val }
defp increment_value ( val , _limit ) , do : { :commit , val + 1 }
2020-02-27 16:46:05 +01:00
defp incorporate_conn_info ( action_settings , %{
assigns : %{ user : % User { id : user_id } } ,
params : params
} ) do
Map . merge ( action_settings , %{
2019-11-11 13:13:06 +01:00
mode : :user ,
conn_params : params ,
conn_info : " #{ user_id } "
} )
end
2020-02-27 16:46:05 +01:00
defp incorporate_conn_info ( action_settings , %{ params : params } = conn ) do
Map . merge ( action_settings , %{
2019-11-11 13:13:06 +01:00
mode : :anon ,
conn_params : params ,
conn_info : " #{ ip ( conn ) } "
} )
end
defp ip ( %{ remote_ip : remote_ip } ) do
remote_ip
|> Tuple . to_list ( )
|> Enum . join ( " . " )
end
defp render_throttled_error ( conn ) do
conn
|> render_error ( :too_many_requests , " Throttled " )
|> halt ( )
end
2020-02-27 16:46:05 +01:00
defp make_key_name ( action_settings ) do
2019-11-11 13:13:06 +01:00
" "
2020-02-27 16:46:05 +01:00
|> attach_selected_params ( action_settings )
|> attach_identity ( action_settings )
2019-11-11 13:13:06 +01:00
end
defp get_scale ( _ , { scale , _ } ) , do : scale
defp get_scale ( :anon , [ { scale , _ } , { _ , _ } ] ) , do : scale
defp get_scale ( :user , [ { _ , _ } , { scale , _ } ] ) , do : scale
defp get_limits ( %{ limits : { _scale , limit } } ) , do : limit
defp get_limits ( %{ mode : :user , limits : [ _ , { _ , limit } ] } ) , do : limit
defp get_limits ( %{ limits : [ { _ , limit } , _ ] } ) , do : limit
2020-02-27 16:46:05 +01:00
defp make_bucket_name ( %{ mode : :user , name : bucket_name_root } ) ,
do : user_bucket_name ( bucket_name_root )
2019-11-11 13:13:06 +01:00
2020-02-27 16:46:05 +01:00
defp make_bucket_name ( %{ mode : :anon , name : bucket_name_root } ) ,
do : anon_bucket_name ( bucket_name_root )
2019-11-11 13:13:06 +01:00
2020-02-27 16:46:05 +01:00
defp attach_selected_params ( input , %{ conn_params : conn_params , opts : plug_opts } ) do
params_string =
plug_opts
2019-11-11 13:13:06 +01:00
|> Keyword . get ( :params , [ ] )
|> Enum . sort ( )
|> Enum . map ( & Map . get ( conn_params , &1 , " " ) )
|> Enum . join ( " : " )
2020-02-27 16:46:05 +01:00
[ input , params_string ]
|> Enum . join ( " : " )
|> String . replace_leading ( " : " , " " )
2019-11-11 13:13:06 +01:00
end
2020-02-28 15:35:01 +01:00
defp initialize_buckets! ( %{ name : _name , limits : nil } ) , do : :ok
2019-11-11 13:13:06 +01:00
2020-02-28 15:35:01 +01:00
defp initialize_buckets! ( %{ name : name , limits : limits } ) do
{ :ok , _pid } =
LimiterSupervisor . add_or_return_limiter ( anon_bucket_name ( name ) , get_scale ( :anon , limits ) )
{ :ok , _pid } =
LimiterSupervisor . add_or_return_limiter ( user_bucket_name ( name ) , get_scale ( :user , limits ) )
:ok
2019-11-11 13:13:06 +01:00
end
defp attach_identity ( base , %{ mode : :user , conn_info : conn_info } ) ,
do : " user: #{ base } : #{ conn_info } "
defp attach_identity ( base , %{ mode : :anon , conn_info : conn_info } ) ,
do : " ip: #{ base } : #{ conn_info } "
2020-02-27 16:46:05 +01:00
defp user_bucket_name ( bucket_name_root ) , do : " user: #{ bucket_name_root } " |> String . to_atom ( )
defp anon_bucket_name ( bucket_name_root ) , do : " anon: #{ bucket_name_root } " |> String . to_atom ( )
2019-11-11 13:13:06 +01:00
end