parent
aa139e9618
commit
320617fa26
@ -0,0 +1,4 @@ |
||||
# Used by "mix format" |
||||
[ |
||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] |
||||
] |
@ -0,0 +1,24 @@ |
||||
# The directory Mix will write compiled artifacts to. |
||||
/_build/ |
||||
|
||||
# If you run "mix test --cover", coverage assets end up here. |
||||
/cover/ |
||||
|
||||
# The directory Mix downloads your dependencies sources to. |
||||
/deps/ |
||||
|
||||
# Where 3rd-party dependencies like ExDoc output generated docs. |
||||
/doc/ |
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally. |
||||
/.fetch |
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too. |
||||
erl_crash.dump |
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build"). |
||||
*.ez |
||||
|
||||
# Ignore package tarball (built via "mix hex.build"). |
||||
dough-*.tar |
||||
|
@ -1,21 +1,21 @@ |
||||
# Edoh |
||||
# Dough |
||||
|
||||
**TODO: Add description** |
||||
|
||||
## Installation |
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed |
||||
by adding `edoh` to your list of dependencies in `mix.exs`: |
||||
by adding `dough` to your list of dependencies in `mix.exs`: |
||||
|
||||
```elixir |
||||
def deps do |
||||
[ |
||||
{:edoh, "~> 0.1.0"} |
||||
{:dough, "~> 0.1.0"} |
||||
] |
||||
end |
||||
``` |
||||
|
||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) |
||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can |
||||
be found at [https://hexdocs.pm/edoh](https://hexdocs.pm/edoh). |
||||
be found at [https://hexdocs.pm/dough](https://hexdocs.pm/dough). |
||||
|
||||
|
@ -0,0 +1,30 @@ |
||||
# This file is responsible for configuring your application |
||||
# and its dependencies with the aid of the Mix.Config module. |
||||
use Mix.Config |
||||
|
||||
# This configuration is loaded before any dependency and is restricted |
||||
# to this project. If another project depends on this project, this |
||||
# file won't be loaded nor affect the parent project. For this reason, |
||||
# if you want to provide default values for your application for |
||||
# 3rd-party users, it should be done in your "mix.exs" file. |
||||
|
||||
# You can configure your application as: |
||||
# |
||||
# config :dough, key: :value |
||||
# |
||||
# and access this configuration in your application as: |
||||
# |
||||
# Application.get_env(:dough, :key) |
||||
# |
||||
# You can also configure a 3rd-party app: |
||||
# |
||||
# config :logger, level: :info |
||||
# |
||||
|
||||
# It is also possible to import configuration files, relative to this |
||||
# directory. For example, you can emulate configuration per environment |
||||
# by uncommenting the line below and defining dev.exs, test.exs and such. |
||||
# Configuration from the imported file will override the ones defined |
||||
# here (which is why it is important to import them last). |
||||
# |
||||
# import_config "#{Mix.env}.exs" |
@ -0,0 +1,36 @@ |
||||
defmodule Dough do |
||||
@moduledoc false |
||||
|
||||
use Application |
||||
|
||||
import Supervisor.Spec |
||||
import Cachex.Spec |
||||
|
||||
|
||||
def start(_type, _args) do |
||||
# List all child processes to be supervised |
||||
children = [ |
||||
# Starts a worker by calling: Dough.Worker.start_link(arg) |
||||
# {Dough.Worker, arg}, |
||||
# Plug.Adapters.Cowboy.child_spec(:https, Dough.Router, [], port: 8331, keyfile: "priv/ssl/localhost.key", certfile: "priv/ssl/localhost.crt", otp_app: :dough) |
||||
worker(Cachex, [:dough, [ |
||||
expiration: expiration( |
||||
default: :timer.seconds(6000), |
||||
interval: :timer.seconds(300), |
||||
lazy: true) |
||||
]]), |
||||
|
||||
{Plug.Adapters.Cowboy2, scheme: :https, plug: Dough.Router, options: [ |
||||
port: 8331, |
||||
otp_app: :dough, |
||||
keyfile: "priv/ssl/localhost.key", |
||||
certfile: "priv/ssl/localhost.crt" |
||||
]} |
||||
] |
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html |
||||
# for other strategies and supported options |
||||
opts = [strategy: :one_for_one, name: Dough.Supervisor] |
||||
Supervisor.start_link(children, opts) |
||||
end |
||||
end |
@ -0,0 +1,26 @@ |
||||
defmodule Dough.RequestContext do |
||||
require Logger |
||||
|
||||
|
||||
defstruct [:start, :close, :lookup, :cache, :ttl] |
||||
|
||||
def ctx_start(ctx) do |
||||
Map.put(ctx, :start, System.monotonic_time(:milliseconds)) |
||||
end |
||||
def ctx_close(ctx) do |
||||
Map.put(ctx, :close, System.monotonic_time(:milliseconds)) |
||||
end |
||||
def ctx_lookup(ctx, dns_record) do |
||||
Map.put(ctx, :lookup, dns_record.qdlist |> List.first()) |
||||
end |
||||
def ctx_cachehit(ctx, v) do |
||||
Map.put(ctx, :cache, v) |
||||
end |
||||
def ctx_ttl(ctx, ttl) do |
||||
Map.put(ctx, :ttl, ttl) |
||||
end |
||||
|
||||
def ctx_log_out(ctx) do |
||||
Logger.info "#{ctx.lookup.type} - #{ctx.lookup.domain} - TTL #{ctx.ttl} | #{ctx.cache} - #{ctx.close - ctx.start}ms" |
||||
end |
||||
end |
@ -0,0 +1,82 @@ |
||||
defmodule Dough.DoHPlug do |
||||
import Socket.Datagram, only: [send!: 3, recv!: 1] |
||||
import Plug.Conn |
||||
import Dough.TTL |
||||
import Dough.RequestContext |
||||
|
||||
require Logger |
||||
require DNS |
||||
|
||||
def init(options), do: options |
||||
|
||||
def call(conn, _opts) do |
||||
|
||||
ctx = %Dough.RequestContext{} |> ctx_start() |
||||
|
||||
dns_message = case parse_doh(conn) do |
||||
{:ok, msg} -> msg |
||||
{:error, _} -> |
||||
Logger.error conn.method |
||||
raise Dough.NotAllowed, "" |
||||
end |
||||
|
||||
{cache_hit, ttl, dns_resp} = fetch_dns(dns_message) |
||||
|
||||
decoded = DNS.Record.decode(dns_message) |
||||
|
||||
ctx = ctx_lookup(ctx, decoded) |
||||
|> ctx_cachehit(cache_hit) |
||||
|> ctx_ttl(ttl) |
||||
|> ctx_close() |
||||
|
||||
Dough.RequestContext.ctx_log_out(ctx) |
||||
|
||||
conn |
||||
# support for [RFC5861] / cache control extensions |
||||
|> put_resp_header("cache-control", "max-age=#{ttl}, stale-while-revalidate=#{ttl * 2}, stale-if-error=#{ttl * 10}") |
||||
|> put_resp_header("content-type", content_type(conn)) |
||||
|> send_resp(200, dns_resp) |
||||
end |
||||
|
||||
def content_type(conn) do |
||||
case List.keyfind(conn.req_headers, "accept", 0) do |
||||
{"accept", accept} -> accept |
||||
nil -> "application/dns-message" |
||||
end |
||||
end |
||||
|
||||
def fetch_dns(dns_message) do |
||||
case ttlcache_lookup(dns_message) do |
||||
nil -> |
||||
resp = handoff_dns(dns_message) |
||||
ttl = ttl_extract(resp) |
||||
ttlcache_set(dns_message, resp, ttl) |
||||
|
||||
{:miss, ttl, resp} |
||||
result -> |
||||
ttl = ttl_extract(result) |
||||
{:hit, ttl, result} |
||||
end |
||||
|
||||
end |
||||
|
||||
def parse_doh(conn) do |
||||
conn = fetch_query_params conn |
||||
case conn.method do |
||||
"GET" -> {:ok, Base.url_decode64!(conn.query_params["dns"], padding: false, ignore: :whitespace)} |
||||
"POST" -> |
||||
{:ok, body, _conn} = read_body(conn) |
||||
{:ok, body} |
||||
_ -> {:error, nil} |
||||
end |
||||
end |
||||
|
||||
def handoff_dns(dns_message) do |
||||
client = Socket.UDP.open! |
||||
send!(client, dns_message, {"8.8.8.8", 53}) |
||||
{data, _server} = recv!(client) |
||||
data |
||||
end |
||||
|
||||
end |
||||
|
@ -0,0 +1,13 @@ |
||||
defmodule Dough.NotAllowed do |
||||
defexception [plug_status: 405, message: "Method Not Allowed"] |
||||
end |
||||
|
||||
defmodule Dough.ServerError do |
||||
defexception [plug_status: 500, message: "Server Error"] |
||||
end |
||||
|
||||
defimpl Plug.Exception, for: [Dough.NotAllowed, Dough.ServerError] do |
||||
def status(exc) do |
||||
exc.plug_status |
||||
end |
||||
end |
@ -0,0 +1,18 @@ |
||||
defmodule Dough.Router do |
||||
use Plug.Router |
||||
use Plug.ErrorHandler |
||||
|
||||
if Mix.env == :dev do |
||||
use Plug.Debugger |
||||
end |
||||
|
||||
require Logger |
||||
|
||||
plug :match |
||||
plug :dispatch |
||||
|
||||
get "/", do: send_resp(conn, 200, "") |
||||
match "/dns-query", to: Dough.DoHPlug |
||||
match _, do: send_resp(conn, 404, "oopsie") |
||||
|
||||
end |
@ -0,0 +1,42 @@ |
||||
defmodule Dough.TTL do |
||||
|
||||
require Logger |
||||
|
||||
def ttlcache_lookup(dns_message) do |
||||
case Cachex.get(:dough, _hashify_dns(dns_message)) do |
||||
{:ok, nil} -> nil |
||||
{:ok, value} -> value |
||||
{:error, :nocache} -> raise Dough.ServerError, message: "Cache failure" |
||||
end |
||||
end |
||||
|
||||
def ttlcache_set(dns_message, dns_result, ttl) do |
||||
Cachex.put(:dough, _hashify_dns(dns_message), dns_result, ttl: :timer.seconds(ttl)) |
||||
dns_result |
||||
end |
||||
|
||||
def ttl_extract(dns_result) do |
||||
decoded = DNS.Record.decode(dns_result) |
||||
answers = {decoded.anlist |> List.first(), decoded.nslist |> List.first()} |
||||
|
||||
case answers do |
||||
{nil, nss} -> |
||||
# {:dns_rr, 'd.akamai.net', :soa, :in, 0, 901, {'n0d.akamai.net', 'hostmaster.akamai.com', 1534350643, 1000, 1000, 1000, 1800}, :undefined, [], false}. |
||||
nss |
||||
|> elem(6) |
||||
# {'n0d.akamai.net', 'hostmaster.akamai.com', 1534350643, 1000, 1000, 1000, 1800} |
||||
|> elem(6) |
||||
|
||||
{ans, nil} -> |
||||
ans.ttl |
||||
{ans, _} -> |
||||
ans.ttl |
||||
end |
||||
end |
||||
|
||||
def _hashify_dns(dns_message) do |
||||
:crypto.hash(:sha256, dns_message) |
||||
|> Base.encode16 |
||||
end |
||||
|
||||
end |
@ -0,0 +1,31 @@ |
||||
defmodule Dough.MixProject do |
||||
use Mix.Project |
||||
|
||||
def project do |
||||
[ |
||||
app: :dough, |
||||
version: "0.1.0", |
||||
elixir: "~> 1.6", |
||||
start_permanent: Mix.env() == :prod, |
||||
deps: deps() |
||||
] |
||||
end |
||||
|
||||
# Run "mix help compile.app" to learn about applications. |
||||
def application do |
||||
[ |
||||
extra_applications: [:logger, :cowboy, :plug, :dns, :cachex], |
||||
mod: {Dough, []} |
||||
] |
||||
end |
||||
|
||||
# Run "mix help deps" to learn about dependencies. |
||||
defp deps do |
||||
[ |
||||
{:cowboy, "~> 2.4.0"}, |
||||
{:plug, "~> 1.5"}, |
||||
{:dns, "~> 2.1.0"}, |
||||
{:cachex, "~> 3.0.3"} |
||||
] |
||||
end |
||||
end |
@ -0,0 +1,12 @@ |
||||
%{ |
||||
"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"}, |
||||
"cowboy": {:hex, :cowboy, "2.4.0", "f1b72fabe9c8a5fc64ac5ac85fb65474d64733d1df52a26fad5d4ba3d9f70a9f", [:rebar3], [{:cowlib, "~> 2.3.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.5.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, |
||||
"cowlib": {:hex, :cowlib, "2.3.0", "bbd58ef537904e4f7c1dd62e6aa8bc831c8183ce4efa9bd1150164fe15be4caa", [:rebar3], [], "hexpm"}, |
||||
"dns": {:hex, :dns, "2.1.0", "4777fe07ae3060c1d5d75024f05c26d7e11fa701d48a6edb9fc305d24cd12c8c", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm"}, |
||||
"eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"}, |
||||
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, |
||||
"plug": {:hex, :plug, "1.6.2", "e06a7bd2bb6de5145da0dd950070110dce88045351224bd98e84edfdaaf5ffee", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, |
||||
"ranch": {:hex, :ranch, "1.5.0", "f04166f456790fee2ac1aa05a02745cc75783c2bfb26d39faf6aefc9a3d3a58a", [:rebar3], [], "hexpm"}, |
||||
"socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm"}, |
||||
"unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"}, |
||||
} |
@ -0,0 +1,18 @@ |
||||
-----BEGIN CERTIFICATE----- |
||||
MIIC5TCCAc2gAwIBAgIJANmlfWvetApSMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV |
||||
BAMMCWxvY2FsaG9zdDAeFw0xODA4MTQxNjIwNDdaFw0xODA5MTMxNjIwNDdaMBQx |
||||
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC |
||||
ggEBALkU8/+EXKF7gplnEvJ6CXs8fKmHZNrEnH5OGZ32E2f1c+ursblHo30zLtn9 |
||||
ahrQUyMn/IaZsN82xIfhHFrkOfSkZ1w1+2n6hhXjNlu7s81tXrhf3LKzWm40SXmE |
||||
lWbcCaxUinYYd11VXO3FJ3Puxie8NAm10RAYksSP+Q3zLjZB4vfD/wT5011+9ds4 |
||||
shr22ldUfG5Isr6/ByI/agkTrHm67YRKwdQ0AR7kaTlHgzKKcWaebH9sdPI4owuZ |
||||
p5VH3nFqA3rtn8x01zYv1u/iN3P0iHl/T9UV2jy0OMq/lJlQR473Ht2Pn/ZiiX+v |
||||
X9vYbI+y+0P3p29c3zVOPTr8XOMCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo |
||||
b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B |
||||
AQsFAAOCAQEANDXhuI9N/4KX15N7ZUd9CD7ljUg9ESNyt1U68r+9229pFZP9JBIw |
||||
Z6XxN3jMR9DSCmiD62qWcbRVdV3GyTbogTwe5N6DATcnIdsz+4ToqXxzXOduolMc |
||||
dzrk7emYYfIVZQ61vCOoSCWYOnQtPTKU7ZetnwFIzBzgqr/ASZjYY2+z7IUnGBSr |
||||
xF6GDh9FMCmsvb5IEaVgbT8aWeo3TwF5UhNJkUnjp+Fr7kKP74EABiUkSNJulUVX |
||||
QFd0PX80I2IbQYKZZNh0Q19ykodDk6gKmhw7ZVVDSCfkpiQIUzFjlCcZZLl2Rn7V |
||||
WK8NU0vqx5/0aNS9ZTir7efp1bkYi6w4+Q== |
||||
-----END CERTIFICATE----- |
@ -0,0 +1,28 @@ |
||||
-----BEGIN PRIVATE KEY----- |
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5FPP/hFyhe4KZ |
||||
ZxLyegl7PHyph2TaxJx+Thmd9hNn9XPrq7G5R6N9My7Z/Woa0FMjJ/yGmbDfNsSH |
||||
4Rxa5Dn0pGdcNftp+oYV4zZbu7PNbV64X9yys1puNEl5hJVm3AmsVIp2GHddVVzt |
||||
xSdz7sYnvDQJtdEQGJLEj/kN8y42QeL3w/8E+dNdfvXbOLIa9tpXVHxuSLK+vwci |
||||
P2oJE6x5uu2ESsHUNAEe5Gk5R4MyinFmnmx/bHTyOKMLmaeVR95xagN67Z/MdNc2 |
||||
L9bv4jdz9Ih5f0/VFdo8tDjKv5SZUEeO9x7dj5/2Yol/r1/b2GyPsvtD96dvXN81 |
||||
Tj06/FzjAgMBAAECggEAYNrONVEXCIqR9aUzDSFABPXKZw0rgjCRlKdaUIeN8EFK |
||||
wHHBN6x9qe82/WzYMeADIcqzI90Z6jXG8zSimg4FJjlCvcaiIvuvX5TfxXGczkwF |
||||
3YaNABdLo5BJQwZXCNTtWvmC2/pBvN4HG4ao9spleDXNJae4GCxD4glV1C8GZ+1+ |
||||
4vakqKsUwU9LqQBC7JwrnDFwlLSutGE7yqpblzZeyO3ByzK8aKfktkZXurfshumk |
||||
LYzvE01e7cb4oOAOmMlEnpQ61y22G12xaEj3HU20/WH3zkS7ijjmxWawErcYxgjh |
||||
KzfEeLg9ERGFQf73fhod1oCkaSjqw/SgnwY2+3kGwQKBgQDgfTcySZNpGCJRm5jH |
||||
CT+j7vubWixzBwcNdILW78V1SaS7s1RMJXgq6bPoyPqKAi1tTsVvpOr3YmuTMexJ |
||||
WG5gDZWWKcpc+ZMvZJGffdoTL4ZbYBrv1mlT0MmC4i/iHIyUNsHsaTcpa1lSvn3g |
||||
mUykg+izOWgOvt9bbwLg+IbIjQKBgQDTD62Gz3dkvo3PXGDUV0p4F4O4IKvHgKlX |
||||
//qDM+yJPfdru6uUWdiUWvdtvWVXyW15cKLZ28lPvIsl4u2Q7ga3t8iIdkbP3RB/ |
||||
G6W+xgPxyOMPazphrLYSd4VF3zEzjcVg6B1uw27uvZDXEHbBgZfvGGbGFkj7x9fz |
||||
Do8IaBR3LwKBgQCVmnzdU3EcmYvNbO88vWoe+tXMqyRyJ93IVrwXN0UVK9XPjOwB |
||||
rvrNRl+yI7XroRXbAaLMC1DXOkzMlHmOS5OLCaKFpyYIHf67l78AViOto39bh8mL |
||||
ygz5YWvZgJk+i54X7AICZf/v521omjBTLHaKMKo0Pm6dXRCG4408lgNkxQKBgHJy |
||||
UOUGALbHQTxM2lCqGL5v+cPRK3SdxrXqHxwf/sYYrN5lQE/MPE2N2hdOmPJ2Tf+I |
||||
3TWHIW00Tru3hpyNBWV/qaSdhh5WwAg8pK6Cz0a/aKhGu7yVG5F97+2IbjSHTp0S |
||||
oEscfD3G2xBTZCHftNQ4hhopoV+t6OJqZZLoZRiPAoGBANZd2Xj8SCENVjVQS8+r |
||||
CoMwZb8lpisdUh1aqpyL7KokvWA2/c6/cEd5A0I+1o63T7N7YUJv+CV3qBeC0u10 |
||||
OIYkn98uzjBMekTYJs/RSDV8nRu766x5KiGqezftH8fMeIMn/fJ9XEIAk9aSMT+F |
||||
KjzV3G0y/iLykshNL+vDW3Ou |
||||
-----END PRIVATE KEY----- |
@ -0,0 +1,8 @@ |
||||
defmodule DoughTest do |
||||
use ExUnit.Case |
||||
doctest Dough |
||||
|
||||
test "greets the world" do |
||||
assert Dough.hello() == :world |
||||
end |
||||
end |
@ -0,0 +1 @@ |
||||
ExUnit.start() |
Loading…
Reference in new issue