Verified Commit d9e4b77f authored by Alexander Strizhakov's avatar Alexander Strizhakov
Browse files

Merge branch 'develop' into gun

parents 814b275a 438394d4
......@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Security
- Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request.
### Removed
- **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
- **Breaking**: OStatus protocol support
......@@ -56,6 +59,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin API: Render whole status in grouped reports
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
- Mastodon API: Limit timeline requests to 3 per timeline per 500ms per user/ip by default.
</details>
### Added
......
......@@ -578,6 +578,7 @@
config :pleroma, :rate_limit,
authentication: {60_000, 15},
timeline: {500, 3},
search: [{1000, 10}, {1000, 30}],
app_account_creation: {1_800_000, 25},
relations_actions: {10_000, 10},
......
......@@ -1903,6 +1903,18 @@
suggestions: [50]
}
]
},
%{
key: :crontab,
type: {:list, :tuple},
description: "Settings for cron background jobs",
suggestions: [
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
{"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
{"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker},
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
]
}
]
},
......@@ -2453,6 +2465,12 @@
description: "For the search requests (account & status search etc.)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :timeline,
type: [:tuple, {:list, :tuple}],
description: "For requests to timelines (each timeline has it's own limiter)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :app_account_creation,
type: [:tuple, {:list, :tuple}],
......@@ -2928,5 +2946,25 @@
description: "A path to custom Elixir modules (such as MRF policies)."
}
]
},
%{
group: :pleroma,
key: :streamer,
type: :group,
description: "Settings for notifications streamer",
children: [
%{
key: :workers,
type: :integer,
description: "Number of workers to send notifications.",
suggestions: [3]
},
%{
key: :overflow_workers,
type: :integer,
description: "Maximum number of workers created if pool is empty.",
suggestions: [2]
}
]
}
]
......@@ -74,11 +74,7 @@
total_user_limit: 3,
enabled: false
config :pleroma, :rate_limit,
search: [{1000, 30}, {1000, 30}],
app_account_creation: {10_000, 5},
password_reset: {1000, 30},
ap_routes: nil
config :pleroma, :rate_limit, %{}
config :pleroma, :http_security, report_uri: "https://endpoint.com"
......
......@@ -343,6 +343,7 @@ Means that:
Supported rate limiters:
* `:search` - Account/Status search.
* `:timeline` - Timeline requests (each timeline has it's own limiter).
* `:app_account_creation` - Account registration from the API.
* `:relations_actions` - Following/Unfollowing in general.
* `:relation_id_action` - Following/Unfollowing for a specific user.
......
......@@ -123,7 +123,7 @@ In addition to that, replace the existing nginx config's contents with the examp
If not an I2P-only instance, add the nginx config below to your existing config at `/etc/nginx/sites-enabled/pleroma.nginx`.
And for both cases, disable CSP in Pleroma's config (STS is disabled by default) so you can define those yourself seperately from the clearnet (if your instance is also on the clearnet).
And for both cases, disable CSP in Pleroma's config (STS is disabled by default) so you can define those yourself separately from the clearnet (if your instance is also on the clearnet).
Copy the following into the `config/prod.secret.exs` in your Pleroma folder (/home/pleroma/pleroma/):
```
config :pleroma, :http_security,
......
......@@ -75,7 +75,7 @@ If not a Tor-only instance,
add the nginx config below to your existing config at `/etc/nginx/sites-enabled/pleroma.nginx`.
---
For both cases, disable CSP in Pleroma's config (STS is disabled by default) so you can define those yourself seperately from the clearnet (if your instance is also on the clearnet).
For both cases, disable CSP in Pleroma's config (STS is disabled by default) so you can define those yourself separately from the clearnet (if your instance is also on the clearnet).
Copy the following into the `config/prod.secret.exs` in your Pleroma folder (/home/pleroma/pleroma/):
```
config :pleroma, :http_security,
......
......@@ -13,6 +13,7 @@ defmodule Pleroma.Pagination do
alias Pleroma.Repo
@default_limit 20
@max_limit 40
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
def page_keys, do: @page_keys
......@@ -130,7 +131,11 @@ defp restrict(query, :offset, %{offset: offset}, _table_binding) do
end
defp restrict(query, :limit, options, _table_binding) do
limit = Map.get(options, :limit, @default_limit)
limit =
case Map.get(options, :limit, @default_limit) do
limit when limit < @max_limit -> limit
_ -> @max_limit
end
query
|> limit(^limit)
......
......@@ -7,8 +7,8 @@ def start_link(init_arg) do
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def add_limiter(limiter_name, expiration) do
{:ok, _pid} =
def add_or_return_limiter(limiter_name, expiration) do
result =
DynamicSupervisor.start_child(
__MODULE__,
%{
......@@ -28,6 +28,12 @@ def add_limiter(limiter_name, expiration) do
]}
}
)
case result do
{:ok, _pid} = result -> result
{:error, {:already_started, pid}} -> {:ok, pid}
_ -> result
end
end
@impl true
......
......@@ -7,12 +7,14 @@ defmodule Pleroma.Plugs.RateLimiter do
## 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:
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.
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`.
......@@ -64,91 +66,102 @@ defmodule Pleroma.Plugs.RateLimiter do
import Pleroma.Web.TranslationHelpers
import Plug.Conn
alias Pleroma.Config
alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
alias Pleroma.User
require Logger
def init(opts) do
limiter_name = Keyword.get(opts, :name)
case Pleroma.Config.get([:rate_limit, limiter_name]) do
nil ->
nil
config ->
name_root = Keyword.get(opts, :bucket_name, limiter_name)
@doc false
def init(plug_opts) do
plug_opts
end
%{
name: name_root,
limits: config,
opts: opts
}
def call(conn, plug_opts) do
if disabled?() do
handle_disabled(conn)
else
action_settings = action_settings(plug_opts)
handle(conn, action_settings)
end
end
# Do not limit if there is no limiter configuration
def call(conn, nil), do: conn
defp handle_disabled(conn) do
if Config.get(:env) == :prod do
Logger.warn("Rate limiter is disabled for localhost/socket")
end
conn
end
def call(conn, settings) do
case disabled?() do
true ->
if Pleroma.Config.get(:env) == :prod,
do: Logger.warn("Rate limiter is disabled for localhost/socket")
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
false ->
settings
|> incorporate_conn_info(conn)
|> check_rate()
|> case do
{:ok, _count} ->
conn
{:error, _count} ->
render_throttled_error(conn)
end
{:error, _count} ->
render_throttled_error(conn)
end
end
def disabled? do
localhost_or_socket =
Pleroma.Config.get([Pleroma.Web.Endpoint, :http, :ip])
Config.get([Pleroma.Web.Endpoint, :http, :ip])
|> Tuple.to_list()
|> Enum.join(".")
|> String.match?(~r/^local|^127.0.0.1/)
remote_ip_disabled = not Pleroma.Config.get([Pleroma.Plugs.RemoteIp, :enabled])
remote_ip_disabled = not Config.get([Pleroma.Plugs.RemoteIp, :enabled])
localhost_or_socket and remote_ip_disabled
end
def inspect_bucket(conn, name_root, settings) do
settings =
settings
|> incorporate_conn_info(conn)
@inspect_bucket_not_found {:error, :not_found}
bucket_name = make_bucket_name(%{settings | name: name_root})
key_name = make_key_name(settings)
limit = get_limits(settings)
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} ->
{:err, :not_found}
case Cachex.get(bucket_name, key_name) do
{:error, :no_cache} ->
@inspect_bucket_not_found
{:ok, nil} ->
{0, limit}
{:ok, nil} ->
{0, limit}
{:ok, value} ->
{value, limit - value}
{:ok, value} ->
{value, limit - value}
end
else
_ -> @inspect_bucket_not_found
end
end
defp check_rate(settings) do
bucket_name = make_bucket_name(settings)
key_name = make_key_name(settings)
limit = get_limits(settings)
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} ->
......@@ -158,8 +171,8 @@ defp check_rate(settings) do
{:error, value}
{:error, :no_cache} ->
initialize_buckets(settings)
check_rate(settings)
initialize_buckets!(action_settings)
check_rate(action_settings)
end
end
......@@ -169,16 +182,19 @@ defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
defp increment_value(val, _limit), do: {:commit, val + 1}
defp incorporate_conn_info(settings, %{assigns: %{user: %User{id: user_id}}, params: params}) do
Map.merge(settings, %{
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(settings, %{params: params} = conn) do
Map.merge(settings, %{
defp incorporate_conn_info(action_settings, %{params: params} = conn) do
Map.merge(action_settings, %{
mode: :anon,
conn_params: params,
conn_info: "#{ip(conn)}"
......@@ -197,10 +213,10 @@ defp render_throttled_error(conn) do
|> halt()
end
defp make_key_name(settings) do
defp make_key_name(action_settings) do
""
|> attach_params(settings)
|> attach_identity(settings)
|> attach_selected_params(action_settings)
|> attach_identity(action_settings)
end
defp get_scale(_, {scale, _}), do: scale
......@@ -215,28 +231,35 @@ defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
defp get_limits(%{limits: [{_, limit}, _]}), do: limit
defp make_bucket_name(%{mode: :user, name: name_root}),
do: user_bucket_name(name_root)
defp make_bucket_name(%{mode: :user, name: bucket_name_root}),
do: user_bucket_name(bucket_name_root)
defp make_bucket_name(%{mode: :anon, name: name_root}),
do: anon_bucket_name(name_root)
defp make_bucket_name(%{mode: :anon, name: bucket_name_root}),
do: anon_bucket_name(bucket_name_root)
defp attach_params(input, %{conn_params: conn_params, opts: opts}) do
param_string =
opts
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}#{param_string}"
[input, params_string]
|> Enum.join(":")
|> String.replace_leading(":", "")
end
defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
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))
defp initialize_buckets(%{name: name, limits: limits}) do
LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits))
LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits))
:ok
end
defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
......@@ -245,6 +268,6 @@ defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
do: "ip:#{base}:#{conn_info}"
defp user_bucket_name(name_root), do: "user:#{name_root}" |> String.to_atom()
defp anon_bucket_name(name_root), do: "anon:#{name_root}" |> String.to_atom()
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
......@@ -10,9 +10,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
alias Pleroma.Pagination
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
# TODO: Replace with a macro when there is a Phoenix release with
# https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
# in it
plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct)
plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public)
plug(RateLimiter, [name: :timeline, bucket_name: :home_timeline] when action == :home)
plug(RateLimiter, [name: :timeline, bucket_name: :hashtag_timeline] when action == :hashtag)
plug(RateLimiter, [name: :timeline, bucket_name: :list_timeline] when action == :list)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
......
......@@ -117,7 +117,7 @@ defp deps do
{:html_entities, "~> 0.5", override: true},
{:phoenix_html, "~> 2.10"},
{:calendar, "~> 0.17.4"},
{:cachex, "~> 3.0.2"},
{:cachex, "~> 3.2"},
{:poison, "~> 3.0", override: true},
# {:tesla, "~> 1.3", override: true},
{:tesla,
......
......@@ -6,7 +6,7 @@
"bbcode": {:hex, :bbcode, "0.1.1", "0023e2c7814119b2e620b7add67182e3f6019f92bfec9a22da7e99821aceba70", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5a981b98ac7d366a9b6bf40eac389aaf4d6e623c631e6b6f8a6b571efaafd338"},
"benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "3aadb1e605747122f60aa7b0b121cca23c14868558157563b3f3e19ea929f7d0"},
"cachex": {:hex, :cachex, "3.2.0", "a596476c781b0646e6cb5cd9751af2e2974c3e0d5498a8cab71807618b74fe2f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "aef93694067a43697ae0531727e097754a9e992a1e7946296f5969d6dd9ac986"},
"calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "738d0e17a93c2ccfe4ddc707bdc8e672e9074c8569498483feb1c4530fb91b2b"},
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]},
"castore": {:hex, :castore, "0.1.5", "591c763a637af2cc468a72f006878584bc6c306f8d111ef8ba1d4c10e0684010", [:mix], [], "hexpm", "6db356b2bc6cc22561e051ff545c20ad064af57647e436650aa24d7d06cd941a"},
......@@ -57,6 +57,7 @@
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"},
"joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
"libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
......@@ -95,6 +96,7 @@
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "825dc00aaba5a1b7c4202a532b696b595dd3bcb3", [ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"]},
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
"swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"},
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#DE2910" d="M36 27c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4V9c0-2.209 1.791-4 4-4h28c2.209 0 4 1.791 4 4v18z"/><path fill="#FFDE02" d="M7 10.951l.929 2.671 2.826.058-2.253 1.708.819 2.706L7 16.479l-2.321 1.615.819-2.706-2.253-1.708 2.826-.058zm6-3.423l.34.688.759.11-.549.536.129.756L13 9.261l-.679.357.13-.756-.55-.536.76-.11zm2 4l.34.688.759.11-.549.536.129.756-.679-.357-.679.357.13-.756-.55-.536.76-.11zm0 4l.34.688.759.11-.549.536.129.756-.679-.357-.679.357.13-.756-.55-.536.76-.11zm-2 3.999l.34.689.759.11-.549.535.129.757-.679-.356-.679.356.13-.757-.55-.535.76-.11z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#DE2910" d="M36 27c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4V9c0-2.209 1.791-4 4-4h28c2.209 0 4 1.791 4 4v18z"/><path fill="#FFDE02" d="M11.136 8.977l.736.356.589-.566-.111.81.72.386-.804.144-.144.804-.386-.72-.81.111.566-.589zm4.665 2.941l-.356.735.566.59-.809-.112-.386.721-.144-.805-.805-.144.721-.386-.112-.809.59.566zm-.957 3.779l.268.772.817.017-.651.493.237.783-.671-.467-.671.467.236-.783-.651-.493.817-.017zm-3.708 3.28l.736.356.589-.566-.111.81.72.386-.804.144-.144.804-.386-.72-.81.111.566-.589zM7 10.951l.929 2.671 2.826.058-2.253 1.708.819 2.706L7 16.479l-2.321 1.615.819-2.706-2.253-1.708 2.826-.058z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#171796" d="M0 27c0 2.209 1.791 4 4 4h28c2.209 0 4-1.791 4-4v-4H0v4z"/><path fill="#EEE" d="M0 13h36v10H0z"/><path fill="#D52B1E" d="M32 5H4C1.791 5 0 6.791 0 9v4h14v7c0 2.209 1.791 4 4 4s4-1.791 4-4v-7h14V9c0-2.209-1.791-4-4-4z"/><path fill="#EEE" d="M15 13h2v2h-2zm2 2h2v2h-2zm2-2h2v2h-2zm0 4h2v2h-2zm-4 0h2v2h-2zm2 2h2v2h-2zm0 3.816V21h-1.816c.301.849.968 1.515 1.816 1.816zm2 0c.849-.302 1.515-.968 1.816-1.816H19v1.816z"/><path fill="#0193DD" d="M18 11.902c.287 0 .57.018.852.043l.159-1.843-1.011-.9-1.011.9.159 1.843c.282-.025.564-.043.852-.043zm4.17.931l.781-1.68-.641-1.191-1.26.499-.481 1.79c.556.148 1.089.343 1.601.582zm-6.742-.582l-.481-1.79-1.257-.5-.642 1.191.781 1.68c.511-.238 1.045-.432 1.599-.581z"/><path fill="#171796" d="M22.368 9.805l-1.292.511-.859-1.087-1.182.728L18 9.034l-1.037.923-1.181-.729-.861 1.089-1.289-.513-.725 1.345.86 1.85.113-.053c.504-.235 1.036-.428 1.579-.574l.026-.007c.552-.147 1.101-.244 1.632-.292l.041-.003c.456-.039 1.226-.039 1.682 0l.037.003c.533.047 1.085.146 1.64.293l.021.006c.541.145 1.072.339 1.579.575l.113.053.86-1.85-.722-1.345zm-8.478 2.862l-.703-1.51.56-1.038 1.095.436.433 1.61c-.475.132-.94.301-1.385.502zm3.372-.857l-.143-1.657.881-.784.881.784-.143 1.657c-.213-.017-.471-.033-.738-.033s-.524.016-.738.033zm4.847.857c-.448-.201-.913-.371-1.386-.504l.432-1.609 1.098-.435.559 1.038-.703 1.51z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#EEE" d="M0 12.9h36v10.2H0z"/><path fill="#171796" d="M36 27c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4v-4h36v4z"/><path fill="#D52B1E" d="M32 5H4C1.791 5 0 6.791 0 9v4h36V9c0-2.209-1.791-4-4-4z"/><path fill="#D52B1E" d="M11.409 7.436V18.97c0 3.64 2.951 6.591 6.591 6.591s6.591-2.951 6.591-6.591V7.436H11.409z"/><path d="M14.25 18h2.5v2.5h-2.5zm2.5 2.5h2.5V23h-2.5zm0-5h2.5V18h-2.5zm2.5 2.5h2.5v2.5h-2.5zm0-5h2.5v2.5h-2.5zm2.5 2.5h2.341V18H21.75zm-7.5-2.5h2.5v2.5h-2.5zm7.5 10h.805c.626-.707 1.089-1.559 1.334-2.5H21.75V23zm-2.5 0v1.931c.929-.195 1.778-.605 2.5-1.171V23h-2.5zm-5 0v-2.5h-2.139c.245.941.707 1.793 1.334 2.5h.805zm-2.341-7.5h2.341V18h-2.341zM14.25 23v.76c.722.566 1.571.976 2.5 1.171V23h-2.5z" fill="#FFF"/><path fill="#171796" d="M24.757 8.141l-1.998.791-1.328-1.682-1.829 1.126L18 6.949l-1.603 1.428-1.826-1.128-1.331 1.684-1.995-.793-1.122 2.08 1.331 2.862.176-.082c.78-.363 1.603-.662 2.443-.888l.04-.011c.854-.227 1.702-.378 2.523-.451l.064-.006c.705-.06 1.896-.06 2.601 0l.058.005c.824.074 1.678.226 2.536.453l.033.009c.836.225 1.658.524 2.441.889l.175.082 1.331-2.861-1.118-2.08z"/><path fill="#0193DD" d="M16.638 8.681l.221 2.563c.33-.026.729-.051 1.141-.051.412 0 .811.025 1.141.051l.221-2.563L18 7.468l-1.362 1.213zm7.941-.053l-1.698.673-.668 2.489c.731.206 1.45.468 2.144.779l1.086-2.336-.864-1.605zm-13.157-.002l-.866 1.606 1.087 2.336c.69-.31 1.409-.572 2.144-.779l-.67-2.49-1.695-.673z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#88C9F9" d="M32 0H4C1.791 0 0 1.791 0 4v22h36V4c0-2.209-1.791-4-4-4z"/><path fill="#66757F" d="M10 26V7l4-4h2l4 4v19zm23-15c0-1-1-1-1-1h-7s-1 0-1 1v15h9V11z"/><path fill="#292F33" d="M28 17c0-1-1-1-1-1h-8c-1 0-1 1-1 1v9h10v-9zm-17 2H6v-5s0-1-1-1H0v13h12v-6s0-1-1-1z"/><path d="M8 21h2v2H8zm8-12h2v2h-2zm0 4h2v2h-2zm-2 4h2v2h-2zm10 1h2v2h-2zm5-6h2v2h-2zm0 4h2v2h-2z" fill="#FFCC4D"/><path fill="#E1E8ED" d="M34 20c-.344 0-.676.047-1 .113-1.677.344-3.045 1.52-3.652 3.085-.34-.567-.804-1.047-1.348-1.418-.714-.487-1.569-.78-2.5-.78-.763 0-1.47.207-2.099.542C22.773 20.611 21.707 20 20.5 20c-.986 0-1.868.415-2.5 1.073-.345.359-.619.788-.788 1.268-.528-.217-1.105-.341-1.712-.341-1.427 0-2.68.677-3.5 1.715-.19.241-.365.495-.504.771C10.892 24.185 10.221 24 9.5 24c-1.058 0-2.013.387-2.78 1-.09.072-.189.134-.274.213-.059-.074-.125-.143-.189-.213-.589-.646-1.369-1.109-2.257-1.288-.263-.053-.533-.087-.812-.087-1.284 0-2.419.591-3.188 1.501V32h36V20.422c-.613-.268-1.288-.422-2-.422z"/><path fill="#CCD6DD" d="M36 27.117c-1.223-2.039-3.449-3.408-6-3.408-2.926 0-5.429 1.796-6.475 4.344C23.35 28.034 23.18 28 23 28c-.702 0-1.369.148-1.976.409C20.291 27.554 19.215 27 18 27c-2.209 0-4 1.791-4 4 0 .05.013.097.015.146C13.689 31.06 13.353 31 13 31c-.876 0-1.679.289-2.338.767C10.065 31.294 9.32 31 8.5 31c-.198 0-.388.026-.577.059C7.286 29.279 5.602 28 3.604 28 2.136 28 .843 28.7 0 29.771V32c0 2.209 1.791 4 4 4h28c2.209 0 4-1.791 4-4v-4.883z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#88C9F9" d="M32 0H4C1.791 0 0 1.791 0 4v6h36V4c0-2.209-1.791-4-4-4z"/><path fill="#E1E8ED" d="M36 16.368V9.257c-.638-.394-1.383-.632-2.188-.632-1.325 0-2.491.627-3.259 1.588C29.75 9.466 28.683 9 27.5 9c-.721 0-1.392.185-1.996.486C24.763 8.018 23.257 7 21.5 7c-.607 0-1.184.124-1.712.342C19.308 5.981 18.024 5 16.5 5c-1.207 0-2.273.611-2.901 1.542C12.97 6.207 12.263 6 11.5 6c-1.641 0-3.062.887-3.848 2.198C6.928 6.33 5.125 5 3 5c-1.131 0-2.162.389-3 1.022v7.955C.838 14.611 24.5 18 24.5 18s10.862-1.238 11.5-1.632z"/><path fill="#CCD6DD" d="M36 14.771C35.157 13.7 33.864 13 32.396 13c-1.997 0-3.681 1.279-4.318 3.059-.19-.033-.38-.059-.578-.059-.82 0-1.565.294-2.162.767C24.679 16.289 23.876 16 23 16c-.353 0-.689.06-1.015.146.002-.049.015-.096.015-.146 0-2.209-1.791-4-4-4-1.215 0-2.291.554-3.024 1.409C14.369 13.148 13.702 13 13 13c-.18 0-.35.034-.525.053C11.429 10.505 8.926 8.709 6 8.709c-2.551 0-4.777 1.369-6 3.408v13.544l32.396-1.452s2.761-1.343 3.604-2.966v-6.472z"/><path fill="#E1E8ED" d="M36 30.499V20.422c-.613-.268-1.288-.422-2-.422-2.125 0-3.928 1.33-4.652 3.198C28.562 21.887 27.141 21 25.5 21c-.763 0-1.47.207-2.099.542C22.773 20.611 21.707 20 20.5 20c-1.524 0-2.808.981-3.288 2.342-.528-.218-1.105-.342-1.712-.342-1.757 0-3.263 1.018-4.004 2.486C10.892 24.185 10.221 24 9.5 24c-1.183 0-2.25.466-3.054 1.213-.768-.961-1.934-1.588-3.259-1.588-1.284 0-2.419.591-3.188 1.501v5.373H36z"/><path fill="#FE5011" d="M36 24.059C32.465 22.229 25.013 17.594 20 9c0 0 0-2-2-2s-2 2-2 2C10.987 17.594 3.535 22.229 0 24.059v2.068c1.044-.495 2.422-1.204 4-2.169V24h2v-1.341c1.284-.88 2.637-1.908 4-3.094V27h2v-9.292c1.384-1.375 2.74-2.923 4-4.655V24h4V13.054c1.26 1.731 2.616 3.28 4 4.655V26h2v-6.435c1.362 1.186 2.716 2.214 4 3.095V25h2v-1.042c1.578.965 2.956 1.674 4 2.169v-2.068z"/><path fill="#F5F8FA" d="M25 25c-.821 0-1.582.249-2.217.673-.664-1.839-2.5-3.07-4.534-2.863-1.883.192-3.348 1.56-3.777 3.298-.181-.012-.363-.019-.55 0-.773.079-1.448.427-1.965.93-.667-.387-1.452-.582-2.278-.498-.333.034-.644.123-.942.236-.003-.047.004-.093 0-.139-.212-2.083-2.073-3.599-4.155-3.387-1.145.117-2.107.742-2.716 1.619-.586-.186-1.217-.258-1.866-.197V32c0 .773.23 1.489.61 2.101C.715 34.098 29 31.209 29 29s-1.791-4-4-4z"/><path fill="#CCD6DD" d="M32 36c2.209 0 4-1.791 4-4v-7.608c-.91-.433-1.925-.683-3-.683-2.926 0-5.429 1.796-6.475 4.344C26.35 28.034 26.18 28 26 28c-.702 0-1.369.147-1.976.409C23.291 27.554 22.215 27 21 27c-2.209 0-4 1.791-4 4 0 .05.013.097.015.146C16.689 31.06 16.353 31 16 31c-.876 0-1.679.289-2.338.767C13.065 31.294 12.32 31 11.5 31c-.198 0-.388.026-.577.059C10.286 29.279 8.602 28 6.604 28c-1.987 0-3.665 1.266-4.31 3.03C2.195 31.022 2.101 31 2 31c-.732 0-1.41.211-2 .555V32c0 2.209 1.791 4 4 4h28z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><circle fill="#CCD6DD" cx="18" cy="18" r="18"/><path fill="#66757F" d="M0 18c0 9.941 8.059 18 18 18 .295 0 .58-.029.87-.043C24.761 33.393 29 26.332 29 18 29 9.669 24.761 2.607 18.87.044 18.58.03 18.295 0 18 0 8.059 0 0 8.059 0 18z"/><circle fill="#5B6876" cx="10.5" cy="8.5" r="3.5"/><circle fill="#5B6876" cx="20" cy="16" r="3"/><circle fill="#5B6876" cx="21.5" cy="27.5" r="3.5"/><circle fill="#5B6876" cx="21" cy="6" r="2"/><circle fill="#5B6876" cx="3" cy="18" r="1"/><circle fill="#B8C5CD" cx="30" cy="9" r="1"/><circle fill="#5B6876" cx="15" cy="31" r="1"/><circle fill="#B8C5CD" cx="32" cy="19" r="2"/><circle fill="#5B6876" cx="10" cy="23" r="2"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><circle fill="#FFD983" cx="18" cy="18" r="18"/><path fill="#66757F" d="M0 18c0 9.941 8.059 18 18 18 .295 0 .58-.029.87-.043C24.761 33.393 29 26.332 29 18 29 9.669 24.761 2.607 18.87.044 18.58.03 18.295 0 18 0 8.059 0 0 8.059 0 18z"/><circle fill="#5B6876" cx="10.5" cy="8.5" r="3.5"/><circle fill="#5B6876" cx="20" cy="16" r="3"/><circle fill="#5B6876" cx="21.5" cy="27.5" r="3.5"/><circle fill="#5B6876" cx="21" cy="6" r="2"/><circle fill="#5B6876" cx="3" cy="18" r="1"/><circle fill="#FFCC4D" cx="30" cy="9" r="1"/><circle fill="#5B6876" cx="15" cy="31" r="1"/><circle fill="#FFCC4D" cx="32" cy="19" r="2"/><circle fill="#5B6876" cx="10" cy="23" r="2"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#CCD6DD" d="M18 0v36c9.941 0 18-8.059 18-18S27.941 0 18 0z"/><path fill="#66757F" d="M0 18c0 9.941 8.059 18 18 18V0C8.059 0 0 8.059 0 18z"/><circle fill="#B8C5CD" cx="25.5" cy="8.5" r="3.5"/><circle fill="#5B6876" cx="12" cy="16" r="3"/><circle fill="#5B6876" cx="13.5" cy="27.5" r="3.5"/><circle fill="#5B6876" cx="15" cy="6" r="2"/><circle fill="#B8C5CD" cx="33" cy="18" r="1"/><circle fill="#5B6876" cx="6" cy="9" r="1"/><circle fill="#B8C5CD" cx="21" cy="31" r="1"/><circle fill="#5B6876" cx="4" cy="19" r="2"/><circle fill="#B8C5CD" cx="26" cy="23" r="2"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFD983" d="M18 0v36c9.941 0 18-8.059 18-18S27.941 0 18 0z"/><path fill="#66757F" d="M0 18c0 9.941 8.059 18 18 18V0C8.059 0 0 8.059 0 18z"/><circle fill="#FFCC4D" cx="25.5" cy="8.5" r="3.5"/><circle fill="#5B6876" cx="12" cy="16" r="3"/><circle fill="#5B6876" cx="13.5" cy="27.5" r="3.5"/><circle fill="#5B6876" cx="15" cy="6" r="2"/><circle fill="#FFCC4D" cx="33" cy="18" r="1"/><circle fill="#5B6876" cx="6" cy="9" r="1"/><circle fill="#FFCC4D" cx="21" cy="31" r="1"/><circle fill="#5B6876" cx="4" cy="19" r="2"/><circle fill="#FFCC4D" cx="26" cy="23" r="2"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#CCD6DD" d="M36 18c0 9.941-8.059 18-18 18-.294 0-.58-.029-.87-.043C11.239 33.393 7 26.332 7 18 7 9.669 11.239 2.607 17.13.044 17.42.03 17.706 0 18 0c9.941 0 18 8.059 18 18z"/><path fill="#66757F" d="M7 18C7 9.669 11.239 2.607 17.13.044 7.596.501 0 8.353 0 18c0 9.646 7.594 17.498 17.128 17.956C11.238 33.391 7 26.331 7 18z"/><circle fill="#B8C5CD" cx="25.5" cy="8.5" r="3.5"/><circle fill="#B8C5CD" cx="16" cy="16" r="3"/><circle fill="#B8C5CD" cx="14.5" cy="27.5" r="3.5"/><circle fill="#B8C5CD" cx="15" cy="6" r="2"/><circle fill="#B8C5CD" cx="33" cy="18" r="1"/><circle fill="#5B6876" cx="6" cy="9" r="1"/><circle fill="#B8C5CD" cx="21" cy="31" r="1"/><circle fill="#5B6876" cx="4" cy="19" r="2"/><circle fill="#B8C5CD" cx="26" cy="23" r="2"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFD983" d="M36 18c0 9.941-8.059 18-18 18-.294 0-.58-.029-.87-.043C11.239 33.393 7 26.332 7 18 7 9.669 11.239 2.607 17.13.044 17.42.03 17.706 0 18 0c9.941 0 18 8.059 18 18z"/><path fill="#66757F" d="M7 18C7 9.669 11.239 2.607 17.13.044 7.596.501 0 8.353 0 18c0 9.646 7.594 17.498 17.128 17.956C11.238 33.391 7 26.331 7 18z"/><circle fill="#FFCC4D" cx="25.5" cy="8.5" r="3.5"/><circle fill="#FFCC4D" cx="16" cy="16" r="3"/><circle fill="#FFCC4D" cx="14.5" cy="27.5" r="3.5"/><circle fill="#FFCC4D" cx="15" cy="6" r="2"/><circle fill="#FFCC4D" cx="33" cy="18" r="1"/><circle fill="#5B6876" cx="6" cy="9" r="1"/><circle fill="#FFCC4D" cx="21" cy="31" r="1"/><circle fill="#5B6876" cx="4" cy="19" r="2"/><circle fill="#FFCC4D" cx="26" cy="23" r="2"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><circle fill="#CCD6DD" cx="18" cy="18" r="18"/><g fill="#B8C5CD"><circle cx="10.5" cy="8.5" r="3.5"/><circle cx="20" cy="17" r="3"/><circle cx="24.5" cy="28.5" r="3.5"/><circle cx="22" cy="5" r="2"/><circle cx="3" cy="18" r="1"/><circle cx="30" cy="9" r="1"/><circle cx="15" cy="31" r="1"/><circle cx="32" cy="19" r="2"/><circle cx="10" cy="23" r="2"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><circle fill="#FFD983" cx="18" cy="18" r="18"/><g fill="#FFCC4D"><circle cx="10.5" cy="8.5" r="3.5"/><circle cx="20" cy="17" r="3"/><circle cx="24.5" cy="28.5" r="3.5"/><circle cx="22" cy="5" r="2"/><circle cx="3" cy="18" r="1"/><circle cx="30" cy="9" r="1"/><circle cx="15" cy="31" r="1"/><circle cx="32" cy="19" r="2"/><circle cx="10" cy="23" r="2"/></g></svg>
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment