aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorSam Seay <sam@manuka.co>2016-09-01 09:43:23 +1200
committerGitHub <noreply@github.com>2016-09-01 09:43:23 +1200
commit3b5120b087182cb96a1a2082a5fb149276e45304 (patch)
tree53f44594fbac8a78ddf7d1fec99779ba310f4e00 /lib
parent7ff101f0fa5aaf52df07d37584472521c4b3cc55 (diff)
downloadrecaptcha-3b5120b087182cb96a1a2082a5fb149276e45304.tar.gz
recaptcha-3b5120b087182cb96a1a2082a5fb149276e45304.tar.xz
Feature/v2 rewrite (#7)
Rewrite of the API. * Change the copyright holder since its a rewrite. * Bump dependencies to their latest versions. * Revision of the README to document the new API * Break config up into keyword list (no longer a map) * A rewrite of the recaptcha verify API and move of templating into Recaptcha.Template * Add tests and credo for code style * Remove exception raising method calls (`HTTPoison.post!` and `Poison.decode!`) * Change Elixir version to 1.2 for `with` support.
Diffstat (limited to 'lib')
-rw-r--r--lib/recaptcha.ex86
-rw-r--r--lib/recaptcha/http.ex37
-rw-r--r--lib/recaptcha/http/mock_http_client.ex18
-rw-r--r--lib/recaptcha/response.ex8
-rw-r--r--lib/recaptcha/template.ex25
-rw-r--r--lib/template.html.eex8
6 files changed, 124 insertions, 58 deletions
diff --git a/lib/recaptcha.ex b/lib/recaptcha.ex
index e0656c3..0037480 100644
--- a/lib/recaptcha.ex
+++ b/lib/recaptcha.ex
@@ -1,75 +1,53 @@
defmodule Recaptcha do
- require Elixir.EEx
+ @moduledoc """
+ A module for verifying reCAPTCHA version 2.0 response strings.
- @secret_key_errors ~w(missing-input-secret invalid-input-secret)
-
- EEx.function_from_file :defp, :render_template, "lib/template.html.eex", [:assigns]
-
- @doc """
- Returns a string with reCAPTCHA code
-
- To convert the string to html code, use Phoenix.HTML.Raw/1 method
+ See the [documentation](https://developers.google.com/recaptcha/docs/verify) for more details.
"""
- def display(options \\ []) do
- public_key = options[:public_key] || config.public_key
- render_template(public_key: public_key, options: options)
- end
+ @secret Application.get_env(:recaptcha, :secret)
+ @http_client Application.get_env(:recaptcha, :http_client, Recaptcha.Http)
@doc """
- Verifies reCAPTCHA response string.
+ Verifies a reCAPTCHA response string.
- The function returns :ok or :error, depending on the verification's result
+ ## Options
- :ok is returned when the response code was successfully verified
+ * `:timeout` - the timeout for the request (defaults to 5000ms)
+ * `:secret` - the secret key used by recaptcha (defaults to the secret provided in application config)
+ * `:remote_ip` - the IP address of the user (optional and not set by default)
- :error is returned when the response is nil or if the reCAPTCHA server returned
- a missing-input-response or invalid-input-response code
+ ## Example
- The function raises RuntimeError if the server returned a missing-input-secret or invalid-input-secret
- code
+ {:ok, api_response} = Recaptcha.verify("response_string")
"""
- def verify(remote_ip, response, options \\ [])
+ @spec verify(String.t, [timeout: integer, secret: String.t, remote_ip: String.t]) :: {:ok, Recaptcha.Response.t} | {:error, [atom]}
+ def verify(response, options \\ [])
- def verify(remote_ip, response, options) when is_tuple(remote_ip) do
- verify(:inet_parse.ntoa(remote_ip), response, options)
+ def verify(nil = _response, _) do
+ {:error, [:invalid_input_response]}
end
- def verify(_remote_ip, nil = _response, _options), do: :error
-
- def verify(remote_ip, response, options) do
- case api_response(remote_ip, response, options) do
- %{"success" => true} ->
- :ok
- %{"success" => false, "error-codes" => error_codes} ->
- handle_error_codes(error_codes)
- %{"success" => false} ->
- :error
+ def verify(response, options) do
+ case @http_client.request_verification(request_body(response, options), Keyword.take(options, [:timeout])) do
+ {:error, errors} -> {:error, errors}
+ {:ok, %{"success" => false, "error-codes" => errors}} -> {:error, Enum.map(errors, fn(error) -> atomise_api_error(error) end) }
+ {:ok, %{"success" => true, "challenge_ts" => timestamp, "hostname" => host}} -> {:ok, %Recaptcha.Response{challenge_ts: timestamp, hostname: host}}
end
end
- defp api_response(remote_ip, response, options) do
- private_key = options[:private_key] || config.private_key
- timeout = options[:timeout] || 5000
- body_content = URI.encode_query(%{"remoteip" => to_string(remote_ip),
- "response" => response,
- "secret" => private_key})
- headers = [{"Content-type", "application/x-www-form-urlencoded"}, {"Accept", "application/json"}]
+ defp request_body(response, options) do
+ body_options = Keyword.take(options, [:remote_ip, :secret])
+ application_options = [secret: @secret]
- HTTPoison.post!(config.verify_url, body_content, headers, timeout: timeout).body
- |> Poison.decode!
+ # override application secret with options secret if it exists
+ application_options
+ |> Keyword.merge(body_options)
+ |> Keyword.put(:response, response)
+ |> URI.encode_query
end
- defp config do
- Application.get_env(:recaptcha, :api_config)
+ defp atomise_api_error(error) do
+ String.replace(error, "-", "_")
+ |> String.to_atom
end
-
- defp handle_error_codes(error_codes) do
- if Enum.any?(error_codes, fn(code) -> Enum.member?(@secret_key_errors, code) end) do
- raise RuntimeError,
- message: "reCaptcha API has declined the private key. Please make sure you've set the correct private key"
- else
- :error
- end
- end
-
end
diff --git a/lib/recaptcha/http.ex b/lib/recaptcha/http.ex
new file mode 100644
index 0000000..01c088a
--- /dev/null
+++ b/lib/recaptcha/http.ex
@@ -0,0 +1,37 @@
+defmodule Recaptcha.Http do
+ @moduledoc """
+ Responsible for managing HTTP requests to the reCAPTCHA API
+ """
+ @headers [{"Content-type", "application/x-www-form-urlencoded"}, {"Accept", "application/json"}]
+ @url Application.get_env(:recaptcha, :verify_url)
+ @timeout Application.get_env(:recaptcha, :timeout, 5000)
+
+ @doc """
+ Sends an HTTP request to the reCAPTCHA version 2.0 API.
+
+ See the [documentation](https://developers.google.com/recaptcha/docs/verify#api-response) for more details on the API response.
+
+ ## Options
+
+ * `:timeout` - the timeout for the request (defaults to 5000ms)
+
+ ## Example
+
+ {:ok, %{ "success" => success, "challenge_ts" => ts, "hostname" => host, "error-codes" => errors}} = Recaptcha.Http.request_verification(%{ secret: "secret", response: "response", remote_ip: "remote_ip"})
+ """
+ @spec request_verification(map, [timeout: integer]) :: {:ok, map} | {:error, [atom]}
+ def request_verification(body, options \\ []) do
+ result =
+ with {:ok, response} <- HTTPoison.post(@url, body, @headers, timeout: options[:timeout] || @timeout),
+ {:ok, data} <- Poison.decode(response.body) do
+ {:ok, data}
+ end
+
+ case result do
+ {:ok, data} -> {:ok, data}
+ {:error, :invalid} -> {:error, [:invalid_api_response]}
+ {:error, {:invalid, _reason}} -> {:error, [:invalid_api_response]}
+ {:error, %{reason: reason}} -> {:error, [reason]}
+ end
+ end
+end
diff --git a/lib/recaptcha/http/mock_http_client.ex b/lib/recaptcha/http/mock_http_client.ex
new file mode 100644
index 0000000..2355dd3
--- /dev/null
+++ b/lib/recaptcha/http/mock_http_client.ex
@@ -0,0 +1,18 @@
+defmodule Recaptcha.Http.MockClient do
+ @moduledoc """
+ A mock HTTP client used for testing.
+ """
+
+ def request_verification(body, options \\ [])
+
+ def request_verification("response=valid_response&secret=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" = body, options) do
+ send self(), {:request_verification, body, options}
+ {:ok, %{"success" => true, "challenge_ts" => "timestamp", "hostname" => "localhost"}}
+ end
+
+ # every other match is a pass through to the real client
+ def request_verification(body, options) do
+ send self(), {:request_verification, body, options}
+ Recaptcha.Http.request_verification(body, options)
+ end
+end
diff --git a/lib/recaptcha/response.ex b/lib/recaptcha/response.ex
new file mode 100644
index 0000000..f8adb18
--- /dev/null
+++ b/lib/recaptcha/response.ex
@@ -0,0 +1,8 @@
+defmodule Recaptcha.Response do
+ @moduledoc """
+ A struct representing the successful recaptcha response from the reCAPTCHA API.
+ """
+ defstruct challenge_ts: "", hostname: ""
+
+ @type t :: %__MODULE__{challenge_ts: String.t, hostname: String.t}
+end
diff --git a/lib/recaptcha/template.ex b/lib/recaptcha/template.ex
new file mode 100644
index 0000000..d644266
--- /dev/null
+++ b/lib/recaptcha/template.ex
@@ -0,0 +1,25 @@
+defmodule Recaptcha.Template do
+ @moduledoc """
+ Responsible for rendering boilerplate recaptcha HTML code, supports noscript fallback.
+
+ Currently the [explicit render](https://developers.google.com/recaptcha/docs/display#explicit_render) functionality
+ is not supported.
+
+ In future this module may be separated out into a Phoenix specific library.
+ """
+ require Elixir.EEx
+
+ EEx.function_from_file :defp, :render_template, "lib/template.html.eex", [:assigns]
+
+ @public_key Application.get_env(:recaptcha, :public_key)
+
+ @doc """
+ Returns a string with reCAPTCHA code
+
+ To convert the string to html code, use Phoenix.HTML.Raw/1 method
+ """
+ def display(options \\ []) do
+ public_key = options[:public_key] || @public_key
+ render_template(public_key: public_key, options: options)
+ end
+end
diff --git a/lib/template.html.eex b/lib/template.html.eex
index c9c5385..5f9109c 100644
--- a/lib/template.html.eex
+++ b/lib/template.html.eex
@@ -2,10 +2,10 @@
<div class="g-recaptcha"
data-sitekey="<%= @public_key %>"
- data-theme="<%= @options[:theme]%>"
- data-type="<%= @options[:type]%>"
- data-tabindex="<%= @options[:tabindex]%>"
- data-size="<%= @options[:size]%>">
+ data-theme="<%= @options[:theme] %>"
+ data-type="<%= @options[:type] %>"
+ data-tabindex="<%= @options[:tabindex] %>"
+ data-size="<%= @options[:size] %>">
</div>
<%= if @options[:noscript] do %>