Skip to content

Commit 5574faf

Browse files
authored
Accommodate for hackney 1.23.0 bug (#220)
1 parent 7083fe2 commit 5574faf

File tree

5 files changed

+86
-25
lines changed

5 files changed

+86
-25
lines changed

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,26 @@ end
3232
Within your application you will need to configure the merchant id and
3333
authorization keys. You do *not* want to put this information in your
3434
`config.exs` file! Either put it in a `{prod,dev,test}.secret.exs` file which is
35-
sourced by `config.exs`, or read the values in from the environment:
35+
sourced by `config.exs`, or read the values in from the environment in
36+
`runtime.exs`:
3637

3738
```elixir
3839
config :braintree,
3940
environment: :sandbox,
40-
master_merchant_id: {:system, "BRAINTREE_MASTER_MERCHANT_ID"},
41-
merchant_id: {:system, "BRAINTREE_MERCHANT_ID"},
42-
public_key: {:system, "BRAINTREE_PUBLIC_KEY"},
43-
private_key: {:system, "BRAINTREE_PRIVATE_KEY"}
41+
master_merchant_id: System.fetch_env!("BRAINTREE_MASTER_MERCHANT_ID"),
42+
merchant_id: System.fetch_env!("BRAINTREE_MERCHANT_ID"),
43+
public_key: System.fetch_env!("BRAINTREE_PUBLIC_KEY"),
44+
private_key: System.fetch_env!("BRAINTREE_PRIVATE_KEY")
4445
```
4546

4647
Furthermore, the environment defaults to `:sandbox`, so you'll want to configure
4748
it with `:production` in `prod.exs`.
4849

50+
Braintree has certificates that will be used for verification during the HTTP
51+
request. This library includes them and will use them by default, but if you
52+
need to override them, you may provide the configuration `:cacertfile` and
53+
`:sandbox_cacertfile`.
54+
4955
You may optionally pass directly those configuration keys to all functions
5056
performing an API call. In that case, those keys will be used to perform the
5157
call.
@@ -60,7 +66,7 @@ config :braintree,
6066
]
6167
```
6268

63-
[opts]: https://github.com/benoitc/hackney/blob/master/doc/hackney.md#request5
69+
[opts]: https://hexdocs.pm/hackney/hackney.html#request/5
6470

6571
## Usage
6672

@@ -130,7 +136,7 @@ Immediately before the HTTP request is fired, a start event will be fired with t
130136
meta data: %{method: method, path: path}
131137
```
132138

133-
Once the HTTP call completes, a stop event will be fired with the following shape:
139+
Once the HTTP call completes, a stop event will be fired with the following shape:
134140

135141
```
136142
event name: [:braintree, :request, :stop]

lib/braintree.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ defmodule Braintree do
8181
end
8282

8383
defp fallback_or_raise(key, nil, nil), do: raise(ConfigError, key)
84+
defp fallback_or_raise(_, nil, default) when is_function(default, 0), do: default.()
8485
defp fallback_or_raise(_, nil, default), do: default
8586
defp fallback_or_raise(_, value, _), do: value
8687
end

lib/http.ex

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@ defmodule Braintree.HTTP do
3030

3131
@type response :: {:ok, map} | error
3232

33-
@production_endpoint "https://api.braintreegateway.com/merchants/"
34-
@sandbox_endpoint "https://api.sandbox.braintreegateway.com/merchants/"
35-
33+
@production_endpoint "https://api.braintreegateway.com/"
3634
@cacertfile "/certs/api_braintreegateway_com.ca.crt"
35+
@sandbox_endpoint "https://api.sandbox.braintreegateway.com/"
3736

3837
@headers [
3938
{"Accept", "application/xml"},
@@ -81,12 +80,14 @@ defmodule Braintree.HTTP do
8180
start_time = System.monotonic_time()
8281

8382
try do
83+
url = build_url(path, opts)
84+
8485
:hackney.request(
8586
method,
86-
build_url(path, opts),
87+
url,
8788
build_headers(opts),
8889
encode_body(body),
89-
build_options()
90+
build_options([{:url, url} | opts])
9091
)
9192
catch
9293
kind, reason ->
@@ -207,17 +208,43 @@ defmodule Braintree.HTTP do
207208
end
208209

209210
@doc false
210-
@spec build_options() :: [...]
211-
def build_options do
212-
cacertfile = Path.join(:code.priv_dir(:braintree), @cacertfile)
211+
@spec build_options(Keyword.t()) :: [...]
212+
def build_options(opts) do
213213
http_opts = Braintree.get_env(:http_options, [])
214+
[:with_body] ++ ssl_opts(opts) ++ http_opts
215+
end
214216

215-
ssl_opts = [
216-
cacertfile: cacertfile,
217-
verify: :verify_peer
218-
]
219-
220-
[:with_body, ssl_options: ssl_opts] ++ http_opts
217+
defp ssl_opts(opts) do
218+
case opts[:url] do
219+
@production_endpoint <> _ ->
220+
[
221+
ssl_options: [
222+
verify: :verify_peer,
223+
# avoid bug in hackney 1.23.0 that compares SSL hostname to resolved IP
224+
server_name_indication: String.to_charlist("api.braintreegateway.com"),
225+
cacertfile:
226+
get_lazy_env(opts, :cacertfile, fn ->
227+
Path.join(:code.priv_dir(:braintree), @cacertfile)
228+
end)
229+
]
230+
]
231+
232+
@sandbox_endpoint <> _ ->
233+
[
234+
ssl_options: [
235+
verify: :verify_peer,
236+
# avoid bug in hackney 1.23.0 that compares SSL hostname to resolved IP
237+
server_name_indication: String.to_charlist("api.sandbox.braintreegateway.com"),
238+
cacertfile:
239+
get_lazy_env(opts, :sandbox_cacertfile, fn ->
240+
Path.join(:code.priv_dir(:braintree), @cacertfile)
241+
end)
242+
]
243+
]
244+
245+
_ ->
246+
[]
247+
end
221248
end
222249

223250
@doc false
@@ -237,14 +264,14 @@ defmodule Braintree.HTTP do
237264
end
238265

239266
defp endpoints do
240-
[production: @production_endpoint, sandbox: sandbox_endpoint()]
267+
[production: @production_endpoint <> "merchants/", sandbox: sandbox_endpoint()]
241268
end
242269

243270
defp sandbox_endpoint do
244271
Application.get_env(
245272
:braintree,
246273
:sandbox_endpoint,
247-
@sandbox_endpoint
274+
@sandbox_endpoint <> "merchants/"
248275
)
249276
end
250277

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
2121
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
2222
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
23-
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
23+
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
2424
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
2525
"plug": {:hex, :plug, "1.15.0", "f40df58e1277fc7189f260daf788d628f03ae3053ce7ac1ca63eaf0423238714", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e434478d1015d968cf98ae2073e78bd63c4a06a94fe328c2df45fcd01df8ae30"},
2626
"plug_cowboy": {:hex, :plug_cowboy, "2.5.1", "7cc96ff645158a94cf3ec9744464414f02287f832d6847079adfe0b58761cbd0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "107d0a5865fa92bcb48e631cc0729ae9ccfa0a9f9a1bd8f01acb513abf1c2d64"},

test/http_test.exs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,40 @@ defmodule Braintree.HTTPTest do
7878

7979
test "build_options/0 considers the application environment" do
8080
with_applicaton_config(:http_options, [timeout: 9000], fn ->
81-
options = HTTP.build_options()
81+
options = HTTP.build_options([])
8282

8383
assert :with_body in options
8484
assert {:timeout, 9000} in options
8585
end)
8686
end
8787

88+
test "build_options/1 adds the cacertfile for production" do
89+
options = HTTP.build_options(url: "https://api.braintreegateway.com/merchants/123foo/")
90+
91+
assert {:ssl_options, ssl_options} = :lists.keyfind(:ssl_options, 1, options)
92+
ssl_options = Map.new(ssl_options)
93+
assert %{cacertfile: _} = ssl_options
94+
assert %{server_name_indication: ~c"api.braintreegateway.com"} = ssl_options
95+
assert %{verify: :verify_peer} = ssl_options
96+
end
97+
98+
test "build_options/1 adds the cacertfile for sandbox" do
99+
options =
100+
HTTP.build_options(url: "https://api.sandbox.braintreegateway.com/merchants/123foo/")
101+
102+
assert {:ssl_options, ssl_options} = :lists.keyfind(:ssl_options, 1, options)
103+
ssl_options = Map.new(ssl_options)
104+
assert %{cacertfile: _} = ssl_options
105+
assert %{server_name_indication: ~c"api.sandbox.braintreegateway.com"} = ssl_options
106+
assert %{verify: :verify_peer} = ssl_options
107+
end
108+
109+
test "build_options/1 does not add the cacertfile for other endpoints" do
110+
options = HTTP.build_options(url: "http://localhost:5000/merchants/123foo/")
111+
112+
refute :lists.keyfind(:ssl_options, 1, options)
113+
end
114+
88115
describe "request/3" do
89116
test "unauthorized response with an invalid merchant id" do
90117
with_applicaton_config(:merchant_id, "junkmerchantid", fn ->

0 commit comments

Comments
 (0)