Meet SourceLevel, an All-in-one Data & Analytics for Engineering Teams

SourceLevel provides metrics and insights by collecting data from many sources such as GitHub and GitLab. Our product brings visibility over every corner of the delivery pipeline in a Data & Analytics Solution for Engineering Teams.

Get started for free
Go to line 1
# TODO: Don't rate limit conditional requests that return 304 Not Modified
Go to line 3
defmodule HexpmWeb.Plugs.Attack do
Go to line 4
  use PlugAttack
Go to line 5
  import HexpmWeb.ControllerHelpers
Go to line 6
  import Plug.Conn
Go to line 7
  alias Hexpm.BlockAddress
Go to line 8
  alias HexpmWeb.RateLimitPubSub
Go to line 10
  @storage {PlugAttack.Storage.Ets, HexpmWeb.Plugs.Attack.Storage}
Go to line 12
  rule "allow local", conn do
Go to line 13
    allow(conn.remote_ip == {127, 0, 0, 1})
Go to line 16
  rule "allow addresses", conn do
Go to line 17
    BlockAddress.try_reload()
Go to line 18
    allow(BlockAddress.allowed?(ip_string(conn.remote_ip)))
Go to line 21
  rule "block addresses", conn do
Go to line 22
    BlockAddress.try_reload()
Go to line 23
    block(BlockAddress.blocked?(ip_string(conn.remote_ip)))
Go to line 26
  rule "user throttle", conn do
Go to line 27
    if user = conn.assigns.current_user do
Go to line 28
      user_throttle(user.id)
Go to line 32
  rule "organization throttle", conn do
Go to line 33
    if organization = conn.assigns.current_organization do
Go to line 34
      organization_throttle(organization.id)
Go to line 38
  rule "ip throttle", conn do
Go to line 39
    ip_throttle(conn.remote_ip)
Go to line 42
  def allow_action(conn, {:throttle, data}, _opts) do
Go to line 43
    add_throttling_headers(conn, data)
Go to line 46
  def allow_action(conn, _data, _opts) do
Go to line 50
  def block_action(conn, {:throttle, data}, _opts) do
Go to line 52
    |> add_throttling_headers(data)
Go to line 53
    |> render_error(429, message: "API rate limit exceeded for #{throttled_user(conn)}")
Go to line 56
  def block_action(conn, _data, _opts) do
Go to line 57
    render_error(conn, 403, message: "Blocked")
Go to line 60
  defp add_throttling_headers(conn, data) do
Go to line 61
    # The expires_at value is a unix time in milliseconds, we want to return one
Go to line 62
    # in seconds
Go to line 63
    reset = div(data[:expires_at], 1_000)
Go to line 66
    |> put_resp_header("x-ratelimit-limit", Integer.to_string(data[:limit]))
Go to line 67
    |> put_resp_header("x-ratelimit-remaining", Integer.to_string(data[:remaining]))
Go to line 68
    |> put_resp_header("x-ratelimit-reset", Integer.to_string(reset))
Go to line 71
  defp throttled_user(conn) do
Go to line 73
      user = conn.assigns.current_user ->
Go to line 74
        "user #{user.id}"
Go to line 76
      organization = conn.assigns.current_organization ->
Go to line 77
        "organization #{organization.id}"
Go to line 80
        "IP #{ip_string(conn.remote_ip)}"
Go to line 84
  defp ip_string({a, b, c, d}) do
Go to line 85
    "#{a}.#{b}.#{c}.#{d}"
Go to line 88
  def user_throttle(user_id, opts \\ []) do
Go to line 89
    key = {:user, user_id}
Go to line 90
    time = opts[:time] || System.system_time(:millisecond)
Go to line 91
    unless opts[:time], do: RateLimitPubSub.broadcast(key, time)
Go to line 93
    timed_throttle(
Go to line 95
      time: time,
Go to line 96
      storage: @storage,
Go to line 97
      limit: 500,
Go to line 98
      period: 60_000
Go to line 102
  def organization_throttle(organization_id, opts \\ []) do
Go to line 103
    key = {:organization, organization_id}
Go to line 104
    time = opts[:time] || System.system_time(:millisecond)
Go to line 105
    unless opts[:time], do: RateLimitPubSub.broadcast(key, time)
Go to line 107
    timed_throttle(
Go to line 109
      time: time,
Go to line 110
      storage: @storage,
Go to line 111
      limit: 500,
Go to line 112
      period: 60_000
Go to line 116
  def ip_throttle(ip, opts \\ []) do
Go to line 117
    key = {:ip, ip}
Go to line 118
    time = opts[:time] || System.system_time(:millisecond)
Go to line 119
    unless opts[:time], do: RateLimitPubSub.broadcast(key, time)
Go to line 121
    timed_throttle(
Go to line 123
      time: time,
Go to line 124
      storage: @storage,
Go to line 125
      limit: 100,
Go to line 126
      period: 60_000
Go to line 130
  # From https://github.com/michalmuskala/plug_attack/blob/812ff857d0958f1a00a711273887d7187ae80a23/lib/rule.ex#L62
Go to line 131
  # Adding an option for `now`
Go to line 132
  defp timed_throttle(key, opts) do
Go to line 134
      do_throttle(key, opts)
Go to line 140
  defp do_throttle(key, opts) do
Go to line 141
    storage = Keyword.fetch!(opts, :storage)
Go to line 142
    limit = Keyword.fetch!(opts, :limit)
Go to line 143
    period = Keyword.fetch!(opts, :period)
Go to line 144
    now = Keyword.fetch!(opts, :time)
Go to line 146
    expires_at = expires_at(now, period)
Go to line 147
    count = do_throttle(storage, key, now, period, expires_at)
Go to line 148
    rem = limit - count
Go to line 149
    data = [period: period, expires_at: expires_at, limit: limit, remaining: max(rem, 0)]
Go to line 150
    {if(rem >= 0, do: :allow, else: :block), {:throttle, data}}
Go to line 153
  defp expires_at(now, period), do: (div(now, period) + 1) * period
Go to line 155
  defp do_throttle({mod, opts}, key, now, period, expires_at) do
Go to line 156
    full_key = {:throttle, key, div(now, period)}
Go to line 157
    mod.increment(opts, full_key, 1, expires_at)