-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathplug_attack.ex
More file actions
199 lines (164 loc) · 5.77 KB
/
Copy pathplug_attack.ex
File metadata and controls
199 lines (164 loc) · 5.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
defmodule PlugAttack do
@moduledoc ~S"""
A plug building toolkit for blocking and throttling abusive requests.
PlugAttack is a set of macros that can be used to build a plug to protect
your web app from bad clients. It allows safelisting, blocklisting and
throttling based on arbitrary properties of the request.
The throttling state is stored in a configurable storage.
By default an implementation backed by `:ets` tables is offered.
## Example
defmodule MyApp.PlugAttack do
use PlugAttack
# For more rules examples see PlugAttack.rule/2 macro documentation.
rule "allow local", conn do
allow conn.remote_ip == {127, 0, 0, 1}
end
# It's possible to customize what happens when conn is let through
def allow_action(conn, _data, _opts), do: conn
# Or when it's blocked
def block_action(conn, _data, _opts) do
conn
|> send_resp(:forbidden, "Forbidden\n")
|> halt
end
end
"""
@typedoc """
The rule return value.
"""
@type rule :: {:allow, term} | {:block, term} | nil
@doc """
Action performed when the request is blocked.
"""
@callback block_action(conn :: Plug.Conn.t, term, term) :: Plug.Conn.t
@doc """
Action performed when the request is allowed.
"""
@callback allow_action(conn :: Plug.Conn.t, term, term) :: Plug.Conn.t
defmacro __using__(opts) do
quote do
@behaviour Plug
@behaviour PlugAttack
@plug_attack_opts unquote(opts)
def init(opts) do
opts
end
def call(conn, opts) do
plug_attack_call(conn, opts)
end
def block_action(conn, _data, _opts) do
conn
|> send_resp(:forbidden, "Forbidden\n")
|> halt
end
def allow_action(conn, _data, _opts) do
conn
end
defoverridable [init: 1, call: 2, block_action: 3, allow_action: 3]
import PlugAttack, only: [rule: 2, rule: 3]
Module.register_attribute(__MODULE__, :plug_attack, accumulate: true)
@before_compile PlugAttack
end
end
@doc false
defmacro __before_compile__(%{module: module} = env) do
plug_attack = Module.get_attribute(module, :plug_attack)
{conn, opts, body} = PlugAttack.compile(env, plug_attack)
quote do
defp plug_attack_call(unquote(conn), unquote(opts)), do: unquote(body)
end
end
@doc ~S"""
Defines a rule.
A rule is an expression that returns either `{:allow, data}`, `{:block, data}`,
`nil` or updated `conn`. If an allow or block tuple is returned we say the rule
*matched*, otherwise the rule didn't match and further rules will be evaluated.
If a rule matched the corresponding `allow_action/3` or `block_action/3`
function on the defining module will be called passing the `conn`,
the `data` value from the allow or block tuple and `opts` as returned by the
`init/1` plug callback. If none rule matched, neither `allow_action/3` nor
`block_action/3` will be called.
Both actions should behave similarly to plugs, returning the modified
`conn` argument. The default implementation of `allow_action/3` will
return the conn unmodified. The default implementation of `block_action/3`
will respond with status 403 Forbidden, the body `"Forbidden\n"` and halt
the plug pipeline.
Various predefined rules are defined in the `PlugAttack.Rule` module.
This module is automatically imported in the rule's body.
## Examples
rule "allow local", conn do
allow conn.remote_ip == {127, 0, 0, 1}
end
rule "block 1.2.3.4", conn do
block conn.remote_ip == {1, 2, 3, 4}
end
rule "throttle per ip", conn do
# throttle to 5 requests per second
throttle conn.remote_ip,
period: 1_000, limit: 5,
storage: {PlugAttack.Storage.Ets, MyApp.PlugAttack.Storage}
end
rule "throttle login requests", conn do
if conn.method == "POST" and conn.path_info == ["login"] do
throttle conn.params["email"],
period: 60_000, limit: 10,
storage: {PlugAttack.Storage.Ets, MyApp.PlugAttack.Storage}
end
end
"""
defmacro rule(message, var \\ quote(do: _), contents) do
contents =
case contents do
[do: block] ->
quote do
import PlugAttack.Rule
unquote(block)
end
_ ->
quote do
import PlugAttack.Rule
try(unquote(contents))
end
end
var = Macro.escape(var)
contents = Macro.escape(contents, unquote: true)
quote bind_quoted: [message: message, var: var, contents: contents] do
name = PlugAttack.register(__MODULE__, message)
defp unquote(name)(unquote(var)), do: unquote(contents)
end
end
@doc false
def register(module, message) do
name = :"rule #{message}"
Module.put_attribute(module, :plug_attack, name)
name
end
@doc false
def compile(env, rules) do
opts = quote(do: opts)
conn = quote(do: conn)
chain = Enum.reduce(rules, conn, "e_rule(&2, &1, conn, opts, env))
{conn, opts, quote do
priv = {unquote(env.module), unquote(opts)}
conn = Plug.Conn.put_private(unquote(conn), :plug_attack, priv)
unquote(chain)
end}
end
defp quote_rule(next, name, conn, opts, _env) do
quote do
case unquote(name)(unquote(conn)) do
{:allow, data} ->
allow_action(unquote(conn), data, unquote(opts))
{:block, data} ->
block_action(unquote(conn), data, unquote(opts))
%Plug.Conn{} = unquote(conn) ->
unquote(next)
nil ->
unquote(next)
other ->
raise "a PlugAttack rule should return `{:allow, data}`, " <>
"`{:block, data}`, `nil`, or `conn`, got: #{inspect other}"
end
end
end
end