Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F12554167
rate_limiter.ex
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
7 KB
Referenced Files
None
Subscribers
None
rate_limiter.ex
View Options
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule
Pleroma.Web.Plugs.RateLimiter
do
@moduledoc
"""
##
Configuration
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:
* 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.
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.
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 10req/1s
* `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
* `foobar` which is disabled
##
Usage
AllowedSyntax:
plug(Pleroma.Web.Plugs.RateLimiter, name: :limiter_name)
plug(Pleroma.Web.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.Web.Plugs.RateLimiter, [name: :one] when action == :one)
plug(Pleroma.Web.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
plug(
Pleroma.Web.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.Web.Plugs.RateLimiter, name: :one)
...
end
"""
import
Pleroma.Web.TranslationHelpers
import
Plug.Conn
alias
Pleroma.Config
alias
Pleroma.User
alias
Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
require
Logger
@cachex
Pleroma.Config
.
get
([
:cachex
,
:provider
],
Cachex
)
@doc
false
def
init
(
plug_opts
)
do
plug_opts
end
def
call
(
conn
,
plug_opts
)
do
if
disabled?
(
conn
)
do
handle_disabled
(
conn
)
else
action_settings
=
action_settings
(
plug_opts
)
handle
(
conn
,
action_settings
)
end
end
defp
handle_disabled
(
conn
)
do
Logger
.
warning
(
"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."
)
conn
end
defp
handle
(
conn
,
nil
),
do
:
conn
defp
handle
(
conn
,
action_settings
)
do
action_settings
|>
incorporate_conn_info
(
conn
)
|>
check_rate
()
|>
case
do
{
:ok
,
_count
}
->
conn
{
:error
,
_count
}
->
render_throttled_error
(
conn
)
end
end
def
disabled?
(
conn
)
do
if
Map
.
has_key?
(
conn
.
assigns
,
:remote_ip_found
),
do
:
!
conn
.
assigns
.
remote_ip_found
,
else
:
false
end
@inspect_bucket_not_found
{
:error
,
:not_found
}
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
)
case
@cachex
.
get
(
bucket_name
,
key_name
)
do
{
:error
,
:no_cache
}
->
@inspect_bucket_not_found
{
:ok
,
nil
}
->
{
0
,
limit
}
{
:ok
,
value
}
->
{
value
,
limit
-
value
}
end
else
_
->
@inspect_bucket_not_found
end
end
def
action_settings
(
plug_opts
)
do
with
limiter_name
when
is_atom
(
limiter_name
)
<-
plug_opts
[
:name
],
limits
when
not
is_nil
(
limits
)
<-
Config
.
get
([
:rate_limit
,
limiter_name
])
do
bucket_name_root
=
Keyword
.
get
(
plug_opts
,
:bucket_name
,
limiter_name
)
%{
name
:
bucket_name_root
,
limits
:
limits
,
opts
:
plug_opts
}
end
end
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
)
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
}
->
initialize_buckets!
(
action_settings
)
check_rate
(
action_settings
)
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
}
defp
incorporate_conn_info
(
action_settings
,
%{
assigns
:
%{
user
:
%
User
{
id
:
user_id
}},
params
:
params
})
do
Map
.
merge
(
action_settings
,
%{
mode
:
:user
,
conn_params
:
params
,
conn_info
:
"
#{
user_id
}
"
})
end
defp
incorporate_conn_info
(
action_settings
,
%{
params
:
params
}
=
conn
)
do
Map
.
merge
(
action_settings
,
%{
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
defp
make_key_name
(
action_settings
)
do
""
|>
attach_selected_params
(
action_settings
)
|>
attach_identity
(
action_settings
)
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
defp
make_bucket_name
(%{
mode
:
:user
,
name
:
bucket_name_root
}),
do
:
user_bucket_name
(
bucket_name_root
)
defp
make_bucket_name
(%{
mode
:
:anon
,
name
:
bucket_name_root
}),
do
:
anon_bucket_name
(
bucket_name_root
)
defp
attach_selected_params
(
input
,
%{
conn_params
:
conn_params
,
opts
:
plug_opts
})
do
params_string
=
plug_opts
|>
Keyword
.
get
(
:params
,
[])
|>
Enum
.
sort
()
|>
Enum
.
map
(
&
Map
.
get
(
conn_params
,
&1
,
""
))
|>
Enum
.
join
(
":"
)
[
input
,
params_string
]
|>
Enum
.
join
(
":"
)
|>
String
.
replace_leading
(
":"
,
""
)
end
defp
initialize_buckets!
(%{
name
:
_name
,
limits
:
nil
}),
do
:
:ok
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
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
}
"
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
()
end
File Metadata
Details
Attached
Mime Type
text/x-ruby
Expires
Sat, Nov 15, 3:10 AM (7 h, 50 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
633134
Default Alt Text
rate_limiter.ex (7 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment