From d8bb2e8142bd740f128fd80a988740e4d50d9d32 Mon Sep 17 00:00:00 2001 From: Nick McKinney Date: Mon, 14 Oct 2019 13:15:09 -0400 Subject: [PATCH 1/3] Implement DOH (RFC 8484, POST method) --- lib/doh.ex | 33 +++++++++++++++++++++++++++++++++ mix.exs | 3 ++- mix.lock | 9 +++++++++ test/dns_test.exs | 15 +++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 lib/doh.ex diff --git a/lib/doh.ex b/lib/doh.ex new file mode 100644 index 0000000..3f46d3c --- /dev/null +++ b/lib/doh.ex @@ -0,0 +1,33 @@ +defmodule DOH do + def resolve(domain, type \\ :a, dns_server \\ {"https://dns.google/dns-query", []}) do + case query(domain, type, dns_server).anlist do + answers when is_list(answers) and length(answers) > 0 -> + data = answers + |> Enum.map(& &1.data) + |> Enum.reject(&is_nil/1) + {:ok, data} + + _ -> + {:error, :not_found} + end + end + + def query(domain, type \\ :a, dns_server \\ {"https://dns.google/dns-query", []}) do + record = %DNS.Record{ + header: %DNS.Header{rd: true}, + qdlist: [%DNS.Query{domain: to_charlist(domain), type: type, class: :in}] + } + + dns_request = DNS.Record.encode(record) + + {http_server, options} = dns_server + case HTTPoison.post(http_server, dns_request, [{"Content-Type", "application/dns-message"}], options) do + {:ok, %HTTPoison.Response{status_code: 200, body: response}} -> + DNS.Record.decode(response) + {:ok, %HTTPoison.Response{status_code: 301}} -> + {:error, "moved"} + {:error, %HTTPoison.Error{reason: reason}} -> + {:error, reason} + end + end +end diff --git a/mix.exs b/mix.exs index bcf299e..e66c59e 100644 --- a/mix.exs +++ b/mix.exs @@ -22,7 +22,8 @@ defmodule DNS.Mixfile do {:socket, "~> 0.3.13"}, {:ex_doc, ">= 0.0.0", only: [:dev]}, {:earmark, ">= 0.0.0", only: [:dev]}, - {:credo, "~> 0.9.0-rc1", only: [:dev, :test], runtime: false} + {:credo, "~> 0.9.0-rc1", only: [:dev, :test], runtime: false}, + {:httpoison, "~> 1.6"}, ] end diff --git a/mix.lock b/mix.lock index 3e0b9de..d4df1be 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,17 @@ %{ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, + "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "credo": {:hex, :credo, "0.9.1", "f021affa11b32a94dc2e807a6472ce0914289c9132f99644a97fc84432b202a1", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.18.2", "993e0a95e9fbb790ac54ea58e700b45b299bd48bc44b4ae0404f28161f37a83e", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, + "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "httpoison": {:hex, :httpoison, "1.6.1", "2ce5bf6e535cd0ab02e905ba8c276580bab80052c5c549f53ddea52d72e81f33", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, } diff --git a/test/dns_test.exs b/test/dns_test.exs index 770ad35..7144cea 100644 --- a/test/dns_test.exs +++ b/test/dns_test.exs @@ -19,6 +19,21 @@ defmodule DNSTest do test "responds with error if domain not found" do assert {:error, :not_found} = DNS.resolve('uifqourefhoqeirhfqeurfhqehfqoerfiuqe.com') end + + test "default DNS-over-HTTPS servers" do + {:ok, results} = DOH.resolve("www.google.com") + + assert is_list(results) + assert length(results) > 0 + end + + test "can query custom DOH server" do + {:ok, results} = + DOH.resolve("www.google.com", :a, {"https://cloudflare-dns.com/dns-query", []}) + + assert is_list(results) + assert length(results) > 0 + end end describe "query" do From 7b7144c2d873768fced870c6a29e9ae6c199d043 Mon Sep 17 00:00:00 2001 From: Nick McKinney Date: Mon, 14 Oct 2019 13:49:32 -0400 Subject: [PATCH 2/3] Move HTTP request to private function --- lib/doh.ex | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/lib/doh.ex b/lib/doh.ex index 3f46d3c..be61be4 100644 --- a/lib/doh.ex +++ b/lib/doh.ex @@ -1,10 +1,12 @@ defmodule DOH do - def resolve(domain, type \\ :a, dns_server \\ {"https://dns.google/dns-query", []}) do - case query(domain, type, dns_server).anlist do + def resolve(domain, type \\ :a, doh_server \\ {"https://dns.google/dns-query", []}) do + case query(domain, type, doh_server).anlist do answers when is_list(answers) and length(answers) > 0 -> - data = answers - |> Enum.map(& &1.data) - |> Enum.reject(&is_nil/1) + data = + answers + |> Enum.map(& &1.data) + |> Enum.reject(&is_nil/1) + {:ok, data} _ -> @@ -12,7 +14,7 @@ defmodule DOH do end end - def query(domain, type \\ :a, dns_server \\ {"https://dns.google/dns-query", []}) do + def query(domain, type \\ :a, doh_server \\ {"https://dns.google/dns-query", []}) do record = %DNS.Record{ header: %DNS.Header{rd: true}, qdlist: [%DNS.Query{domain: to_charlist(domain), type: type, class: :in}] @@ -20,14 +22,32 @@ defmodule DOH do dns_request = DNS.Record.encode(record) - {http_server, options} = dns_server - case HTTPoison.post(http_server, dns_request, [{"Content-Type", "application/dns-message"}], options) do + case request(dns_request, doh_server) do + {:ok, response} -> response + {:error, _} -> %DNS.Record{} + end + end + + defp request(query, doh_server) do + {http_server, options} = doh_server + + case HTTPoison.post( + http_server, + query, + [{"Content-Type", "application/dns-message"}], + options + ) do {:ok, %HTTPoison.Response{status_code: 200, body: response}} -> - DNS.Record.decode(response) - {:ok, %HTTPoison.Response{status_code: 301}} -> - {:error, "moved"} + {:ok, DNS.Record.decode(response)} + + {:ok, %HTTPoison.Response{}} -> + {:error, "query error"} + + {:error, :fmt} -> + {:error, "http error"} + {:error, %HTTPoison.Error{reason: reason}} -> - {:error, reason} - end + {:error, reason} + end end end From 6560e145c52496c6e607df03dc9e4fa96d9b0711 Mon Sep 17 00:00:00 2001 From: Nick McKinney Date: Mon, 14 Oct 2019 13:57:44 -0400 Subject: [PATCH 3/3] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index c5e3217..3f8d3c4 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,13 @@ Server listening at 8000 iex(3)> Process.exit(server_pid, :normal) ``` +### DOH client + +```elixir +iex> DOH.resolve("www.google.com") +{:ok, [{172, 217, 13, 228}]} +``` + For more information, see [API Reference](https://hexdocs.pm/dns/2.1.2/api-reference.html) ## License