From 65dc888f8f6b707cd357b998c01429671cf97119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 22 Jun 2020 17:04:53 +0200 Subject: [PATCH] Do not rely on conditional module definition The downside of conditional module definition is that it may not work as expected in umbrellas. Given the Sentry Logger configuration in an umbrella project is shared across all apps, if we do: config :logger, :backends, [Sentry.LoggerBackend] then `:sentry` needs to be defined on all dependencies. However, if a dependency is not a web application, Sentry may be compiled before Plug! And if it is compiled before Plug, it wouldn't define Sentry.PlugCapture and friends, which means the web app in the umbrella wouldn't have those modules available. --- lib/sentry/plug_capture.ex | 84 +++++----- lib/sentry/plug_context.ex | 314 ++++++++++++++++++------------------- mix.exs | 2 +- 3 files changed, 199 insertions(+), 201 deletions(-) diff --git a/lib/sentry/plug_capture.ex b/lib/sentry/plug_capture.ex index 3475e275..aa077b3f 100644 --- a/lib/sentry/plug_capture.ex +++ b/lib/sentry/plug_capture.ex @@ -1,51 +1,49 @@ -if Code.ensure_loaded?(Plug) do - defmodule Sentry.PlugCapture do - @moduledoc """ - Provides basic functionality to handle and send errors occurring within - Plug applications, including Phoenix. - It is intended for usage with `Sentry.PlugContext`. - - #### Usage - In a Phoenix application, it is important to use this module before - the Phoenix endpoint itself. It should be added to your endpoint.ex: - - - defmodule MyApp.Endpoint - use Sentry.PlugCapture - use Phoenix.Endpoint, otp_app: :my_app - # ... - end +defmodule Sentry.PlugCapture do + @moduledoc """ + Provides basic functionality to handle and send errors occurring within + Plug applications, including Phoenix. + It is intended for usage with `Sentry.PlugContext`. - In a Plug application, it can be added below your router: + #### Usage + In a Phoenix application, it is important to use this module before + the Phoenix endpoint itself. It should be added to your endpoint.ex: - defmodule MyApp.PlugRouter do - use Plug.Router - use Sentry.PlugCapture - # ... - end - """ - defmacro __using__(_opts) do - quote do - @before_compile Sentry.PlugCapture + + defmodule MyApp.Endpoint + use Sentry.PlugCapture + use Phoenix.Endpoint, otp_app: :my_app + # ... end + + In a Plug application, it can be added below your router: + + defmodule MyApp.PlugRouter do + use Plug.Router + use Sentry.PlugCapture + # ... + end + """ + defmacro __using__(_opts) do + quote do + @before_compile Sentry.PlugCapture end + end - defmacro __before_compile__(_) do - quote do - defoverridable call: 2 - - def call(conn, opts) do - try do - super(conn, opts) - rescue - e in Plug.Conn.WrapperError -> - Sentry.capture_exception(e.reason, stacktrace: e.stack, event_source: :plug) - Plug.Conn.WrapperError.reraise(e) - - e -> - Sentry.capture_exception(e, stacktrace: __STACKTRACE__, event_source: :plug) - :erlang.raise(:error, e, __STACKTRACE__) - end + defmacro __before_compile__(_) do + quote do + defoverridable call: 2 + + def call(conn, opts) do + try do + super(conn, opts) + rescue + e in Plug.Conn.WrapperError -> + Sentry.capture_exception(e.reason, stacktrace: e.stack, event_source: :plug) + Plug.Conn.WrapperError.reraise(e) + + e -> + Sentry.capture_exception(e, stacktrace: __STACKTRACE__, event_source: :plug) + :erlang.raise(:error, e, __STACKTRACE__) end end end diff --git a/lib/sentry/plug_context.ex b/lib/sentry/plug_context.ex index 8210fcd3..f0f54cb0 100644 --- a/lib/sentry/plug_context.ex +++ b/lib/sentry/plug_context.ex @@ -1,199 +1,199 @@ -if Code.ensure_loaded?(Plug) do - defmodule Sentry.PlugContext do - @moduledoc """ - This module adds Sentry context metadata during the request in a Plug - application. It includes defaults for scrubbing sensitive data, and - options for customizing it by default. - - It is intended for usage with `Sentry.PlugCapture` as metadata added here - will appear in events captured. - - ### Sending Post Body Params - - In order to send post body parameters you should first scrub them of sensitive - information. By default, they will be scrubbed with - `Sentry.Plug.default_body_scrubber/1`. It can be overridden by passing - the `body_scrubber` option, which accepts a `Plug.Conn` and returns a map - to send. Setting `:body_scrubber` to `nil` will not send any data back. - If you would like to make use of Sentry's default scrubber behavior in a custom - scrubber, it can be called directly. An example configuration may look like - the following: - - def scrub_params(conn) do - # Makes use of the default body_scrubber to avoid sending password - # and credit card information in plain text. To also prevent sending - # our sensitive "my_secret_field" and "other_sensitive_data" fields, - # we simply drop those keys. - Sentry.Plug.default_body_scrubber(conn) - |> Map.drop(["my_secret_field", "other_sensitive_data"]) - end +defmodule Sentry.PlugContext do + @moduledoc """ + This module adds Sentry context metadata during the request in a Plug + application. It includes defaults for scrubbing sensitive data, and + options for customizing it by default. + + It is intended for usage with `Sentry.PlugCapture` as metadata added here + will appear in events captured. + + ### Sending Post Body Params + + In order to send post body parameters you should first scrub them of sensitive + information. By default, they will be scrubbed with + `Sentry.Plug.default_body_scrubber/1`. It can be overridden by passing + the `body_scrubber` option, which accepts a `Plug.Conn` and returns a map + to send. Setting `:body_scrubber` to `nil` will not send any data back. + If you would like to make use of Sentry's default scrubber behavior in a custom + scrubber, it can be called directly. An example configuration may look like + the following: + + def scrub_params(conn) do + # Makes use of the default body_scrubber to avoid sending password + # and credit card information in plain text. To also prevent sending + # our sensitive "my_secret_field" and "other_sensitive_data" fields, + # we simply drop those keys. + Sentry.Plug.default_body_scrubber(conn) + |> Map.drop(["my_secret_field", "other_sensitive_data"]) + end - Then pass it into Sentry.Plug: + Then pass it into Sentry.Plug: - use Sentry.PlugContext, body_scrubber: &scrub_params/1 + use Sentry.PlugContext, body_scrubber: &scrub_params/1 - You can also pass it in as a `{module, fun}` like so: + You can also pass it in as a `{module, fun}` like so: - use Sentry.PlugContext, body_scrubber: {MyModule, :scrub_params} + use Sentry.PlugContext, body_scrubber: {MyModule, :scrub_params} - *Please Note*: If you are sending large files you will want to scrub them out. + *Please Note*: If you are sending large files you will want to scrub them out. - ### Headers Scrubber + ### Headers Scrubber - By default Sentry will scrub Authorization and Authentication headers from all - requests before sending them. It can be configured similarly to the body params - scrubber, but is configured with the `:header_scrubber` key. + By default Sentry will scrub Authorization and Authentication headers from all + requests before sending them. It can be configured similarly to the body params + scrubber, but is configured with the `:header_scrubber` key. - def scrub_headers(conn) do - # default is: Sentry.Plug.default_header_scrubber(conn) - # - # We do not want to include Content-Type or User-Agent in reported - # headers, so we drop them. - Enum.into(conn.req_headers, %{}) - |> Map.drop(["content-type", "user-agent"]) - end + def scrub_headers(conn) do + # default is: Sentry.Plug.default_header_scrubber(conn) + # + # We do not want to include Content-Type or User-Agent in reported + # headers, so we drop them. + Enum.into(conn.req_headers, %{}) + |> Map.drop(["content-type", "user-agent"]) + end - Then pass it into Sentry.Plug: + Then pass it into Sentry.Plug: - use Sentry.PlugContext, header_scrubber: &scrub_headers/1 + use Sentry.PlugContext, header_scrubber: &scrub_headers/1 - It can also be passed in as a `{module, fun}` like so: + It can also be passed in as a `{module, fun}` like so: - use Sentry.PlugContext, header_scrubber: {MyModule, :scrub_headers} + use Sentry.PlugContext, header_scrubber: {MyModule, :scrub_headers} - ### Cookie Scrubber + ### Cookie Scrubber - By default Sentry will scrub all cookies before sending events. - It can be configured similarly to the headers scrubber, but is configured with the `:cookie_scrubber` key. + By default Sentry will scrub all cookies before sending events. + It can be configured similarly to the headers scrubber, but is configured with the `:cookie_scrubber` key. - To configure scrubbing, we can set all configuration keys: + To configure scrubbing, we can set all configuration keys: - use Sentry.PlugContext, header_scrubber: &scrub_headers/1, - body_scrubber: &scrub_params/1, cookie_scrubber: &scrub_cookies/1 + use Sentry.PlugContext, header_scrubber: &scrub_headers/1, + body_scrubber: &scrub_params/1, cookie_scrubber: &scrub_cookies/1 - ### Including Request Identifiers + ### Including Request Identifiers - If you're using Phoenix, Plug.RequestId, or another method to set a request ID - response header, and would like to include that information with errors - reported by Sentry.PlugContext, the `:request_id_header` option allows you to set - which header key Sentry should check. It will default to "x-request-id", - which Plug.RequestId (and therefore Phoenix) also default to. + If you're using Phoenix, Plug.RequestId, or another method to set a request ID + response header, and would like to include that information with errors + reported by Sentry.PlugContext, the `:request_id_header` option allows you to set + which header key Sentry should check. It will default to "x-request-id", + which Plug.RequestId (and therefore Phoenix) also default to. - use Sentry.PlugContext, request_id_header: "application-request-id" - """ - @behaviour Plug + use Sentry.PlugContext, request_id_header: "application-request-id" + """ - @default_scrubbed_param_keys ["password", "passwd", "secret"] - @default_scrubbed_header_keys ["authorization", "authentication", "cookie"] - @credit_card_regex ~r/^(?:\d[ -]*?){13,16}$/ - @scrubbed_value "*********" - @default_plug_request_id_header "x-request-id" + if Code.ensure_loaded?(Plug) do + @behaviour Plug + end - def init(opts) do - opts - end + @default_scrubbed_param_keys ["password", "passwd", "secret"] + @default_scrubbed_header_keys ["authorization", "authentication", "cookie"] + @credit_card_regex ~r/^(?:\d[ -]*?){13,16}$/ + @scrubbed_value "*********" + @default_plug_request_id_header "x-request-id" - def call(conn, opts) do - request = build_request_interface_data(conn, opts) + def init(opts) do + opts + end - Sentry.Context.set_request_context(request) - conn - end + def call(conn, opts) do + request = build_request_interface_data(conn, opts) + Sentry.Context.set_request_context(request) + conn + end - @spec build_request_interface_data(Plug.Conn.t(), keyword()) :: map() - def build_request_interface_data(%Plug.Conn{} = conn, opts) do - body_scrubber = Keyword.get(opts, :body_scrubber, {__MODULE__, :default_body_scrubber}) - - header_scrubber = - Keyword.get(opts, :header_scrubber, {__MODULE__, :default_header_scrubber}) - - cookie_scrubber = - Keyword.get(opts, :cookie_scrubber, {__MODULE__, :default_cookie_scrubber}) - - request_id = Keyword.get(opts, :request_id_header) || @default_plug_request_id_header - - conn = - Plug.Conn.fetch_cookies(conn) - |> Plug.Conn.fetch_query_params() - - %{ - url: Plug.Conn.request_url(conn), - method: conn.method, - data: handle_data(conn, body_scrubber), - query_string: conn.query_string, - cookies: handle_data(conn, cookie_scrubber), - headers: handle_data(conn, header_scrubber), - env: %{ - "REMOTE_ADDR" => remote_address(conn.remote_ip), - "REMOTE_PORT" => Plug.Conn.get_peer_data(conn).port, - "SERVER_NAME" => conn.host, - "SERVER_PORT" => conn.port, - "REQUEST_ID" => Plug.Conn.get_resp_header(conn, request_id) |> List.first() - } + @spec build_request_interface_data(Plug.Conn.t(), keyword()) :: map() + def build_request_interface_data(conn, opts) do + body_scrubber = Keyword.get(opts, :body_scrubber, {__MODULE__, :default_body_scrubber}) + + header_scrubber = + Keyword.get(opts, :header_scrubber, {__MODULE__, :default_header_scrubber}) + + cookie_scrubber = + Keyword.get(opts, :cookie_scrubber, {__MODULE__, :default_cookie_scrubber}) + + request_id = Keyword.get(opts, :request_id_header) || @default_plug_request_id_header + + conn = + Plug.Conn.fetch_cookies(conn) + |> Plug.Conn.fetch_query_params() + + %{ + url: Plug.Conn.request_url(conn), + method: conn.method, + data: handle_data(conn, body_scrubber), + query_string: conn.query_string, + cookies: handle_data(conn, cookie_scrubber), + headers: handle_data(conn, header_scrubber), + env: %{ + "REMOTE_ADDR" => remote_address(conn.remote_ip), + "REMOTE_PORT" => Plug.Conn.get_peer_data(conn).port, + "SERVER_NAME" => conn.host, + "SERVER_PORT" => conn.port, + "REQUEST_ID" => Plug.Conn.get_resp_header(conn, request_id) |> List.first() } - end + } + end - defp remote_address(address) do - address - |> :inet.ntoa() - |> case do - {:error, _} -> - "" + defp remote_address(address) do + address + |> :inet.ntoa() + |> case do + {:error, _} -> + "" - address -> - to_string(address) - end + address -> + to_string(address) end + end - defp handle_data(_conn, nil), do: %{} + defp handle_data(_conn, nil), do: %{} - defp handle_data(conn, {module, fun}) do - apply(module, fun, [conn]) - end + defp handle_data(conn, {module, fun}) do + apply(module, fun, [conn]) + end - defp handle_data(conn, fun) when is_function(fun) do - fun.(conn) - end + defp handle_data(conn, fun) when is_function(fun) do + fun.(conn) + end - @spec default_cookie_scrubber(Plug.Conn.t()) :: map() - def default_cookie_scrubber(_conn) do - %{} - end + @spec default_cookie_scrubber(Plug.Conn.t()) :: map() + def default_cookie_scrubber(_conn) do + %{} + end - @spec default_header_scrubber(Plug.Conn.t()) :: map() - def default_header_scrubber(conn) do - Enum.into(conn.req_headers, %{}) - |> Map.drop(@default_scrubbed_header_keys) - end + @spec default_header_scrubber(Plug.Conn.t()) :: map() + def default_header_scrubber(conn) do + Enum.into(conn.req_headers, %{}) + |> Map.drop(@default_scrubbed_header_keys) + end - @spec default_body_scrubber(Plug.Conn.t()) :: map() - def default_body_scrubber(conn) do - scrub_map(conn.params) - end + @spec default_body_scrubber(Plug.Conn.t()) :: map() + def default_body_scrubber(conn) do + scrub_map(conn.params) + end - defp scrub_map(map) do - Enum.into(map, %{}, fn {key, value} -> - value = - cond do - Enum.member?(@default_scrubbed_param_keys, key) -> - @scrubbed_value + defp scrub_map(map) do + Enum.into(map, %{}, fn {key, value} -> + value = + cond do + Enum.member?(@default_scrubbed_param_keys, key) -> + @scrubbed_value - is_binary(value) && Regex.match?(@credit_card_regex, value) -> - @scrubbed_value + is_binary(value) && Regex.match?(@credit_card_regex, value) -> + @scrubbed_value - is_map(value) && Map.has_key?(value, :__struct__) -> - Map.from_struct(value) - |> scrub_map() + is_map(value) && Map.has_key?(value, :__struct__) -> + Map.from_struct(value) + |> scrub_map() - is_map(value) -> - scrub_map(value) + is_map(value) -> + scrub_map(value) - true -> - value - end + true -> + value + end - {key, value} - end) - end + {key, value} + end) end end diff --git a/mix.exs b/mix.exs index 6a91ac21..3c3da453 100644 --- a/mix.exs +++ b/mix.exs @@ -15,7 +15,7 @@ defmodule Sentry.Mixfile do plt_add_apps: [:mix, :plug, :hackney] ], docs: [extras: ["README.md"], main: "readme"], - xref: [exclude: [:hackney, :hackney_pool]] + xref: [exclude: [:hackney, :hackney_pool, Plug.Conn]] ] end