Skip to content

Commit

Permalink
make it so
Browse files Browse the repository at this point in the history
  • Loading branch information
kf0jvt committed Aug 13, 2016
0 parents commit 554a598
Show file tree
Hide file tree
Showing 31 changed files with 2,274 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# App artifacts
/_build
/db
/deps
/*.ez

# Generated on crash by the VM
erl_crash.dump

# The config/prod.secret.exs file by default contains sensitive
# data and you should not commit it into version control.
#
# Alternatively, you may comment the line below and commit the
# secrets file as long as you replace its contents by environment
# variables.
/config/prod.secret.exs
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Validating GitHub Webhooks with phoenix
## TL;DR

* GitHub hashes the raw body of their messages
* You need to implement a custom parser to calculate the hash and still parse the JSON.
* Attempting to to implement a plug that reads the body before the parser renders the parsers unworkable. The body is read-once.
* This repo is an example of how to do it in Phoenix.
* Environment variables need to be available at compile time.

## What are we going to do?

GitHub web hooks are a pretty cool way to monitor what is going on with
your code on GitHub. Want to take some automated action when someone pushes
to the master branch? Webhooks are very useful for that. However,
you don't want your application to act on just any POST request that comes in.
You need to be sure that the data your app is receiving really came from GitHub.

GitHub provides this assurance by signing the messages that it sends to you.
When you go into settings and set up a web hook you have the option of specifying
a key. GitHub will use that key to create a cryptographic hash of every message it
sends to you. When you receive a message you can calculate the hash yourself and
compare it to the hash GitHub provided thus proving that the message was sent by
some service that knows the key.

There are some challenges we need to get past, though. The biggest one is that you
have to read the body of the incoming request in its original form, in other words before
any parsers have had a chance to alter it. However, reading the body makes the body
inaccessible to anything else that needs to read the message body. So we can't just
make a simple function, we need to replace the JSON parser with a new parser that will
also create a copy of the unaltered body and put that in the `conn`. Let's get started.

## Create project and add dependencies

`mix phoenix.new github_webhooks --no-html --no-brunch`

Add this to `mix.exs`. When we compare the hash value provided by GitHub to the
hash value that we calculate we want to make sure that we're not opening ourselves up
to [timing attacks](https://codahale.com/a-lesson-in-timing-attacks/). So we need to do
a secure comparison instead of a simple `==`.

`{:secure_compare, "~> 0.0.1"},`

and run `mix deps.get`

## Create an endpoint for the webhooks.
27 changes: 27 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config

# General application configuration
config :github_webhooks,
ecto_repos: [GithubWebhooks.Repo]

# Configures the endpoint
config :github_webhooks, GithubWebhooks.Endpoint,
url: [host: "localhost"],
secret_key_base: "YkbRzfc8LFlyv2k0jAFNiC7La7G+IjmGvpRoZbhmw6ZqLIt/Bf1BAt0ZSpa+P/+K",
render_errors: [view: GithubWebhooks.ErrorView, accepts: ~w(json)],
pubsub: [name: GithubWebhooks.PubSub,
adapter: Phoenix.PubSub.PG2]

# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"
31 changes: 31 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use Mix.Config

# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources.
config :github_webhooks, GithubWebhooks.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: []


# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"

# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20

# Configure your database
config :github_webhooks, GithubWebhooks.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "github_webhooks_dev",
hostname: "localhost",
pool_size: 10
65 changes: 65 additions & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use Mix.Config

# For production, we configure the host to read the PORT
# from the system environment. Therefore, you will need
# to set PORT=80 before running your server.
#
# You should also configure the url host to something
# meaningful, we use this information when generating URLs.
#
# Finally, we also include the path to a manifest
# containing the digested version of static files. This
# manifest is generated by the mix phoenix.digest task
# which you typically run after static files are built.
config :github_webhooks, GithubWebhooks.Endpoint,
http: [port: {:system, "PORT"}],
url: [host: "example.com", port: 80],
cache_static_manifest: "priv/static/manifest.json"

# Do not print debug messages in production
config :logger, level: :info

# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to the previous section and set your `:url` port to 443:
#
# config :github_webhooks, GithubWebhooks.Endpoint,
# ...
# url: [host: "example.com", port: 443],
# https: [port: 443,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
#
# Where those two env variables return an absolute path to
# the key and cert in disk or a relative path inside priv,
# for example "priv/ssl/server.key".
#
# We also recommend setting `force_ssl`, ensuring no data is
# ever sent via http, always redirecting to https:
#
# config :github_webhooks, GithubWebhooks.Endpoint,
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.

# ## Using releases
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start the server for all endpoints:
#
# config :phoenix, :serve_endpoints, true
#
# Alternatively, you can configure exactly which server to
# start per endpoint:
#
# config :github_webhooks, GithubWebhooks.Endpoint, server: true
#
# You will also need to set the application root to `.` in order
# for the new static assets to be served after a hot upgrade:
#
# config :github_webhooks, GithubWebhooks.Endpoint, root: "."

# Finally import the config/prod.secret.exs
# which should be versioned separately.
import_config "prod.secret.exs"
19 changes: 19 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use Mix.Config

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :github_webhooks, GithubWebhooks.Endpoint,
http: [port: 4001],
server: false

# Print only warnings and errors during test
config :logger, level: :warn

# Configure your database
config :github_webhooks, GithubWebhooks.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "github_webhooks_test",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox
31 changes: 31 additions & 0 deletions lib/github_webhooks.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule GithubWebhooks do
use Application

# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec

# Define workers and child supervisors to be supervised
children = [
# Start the Ecto repository
supervisor(GithubWebhooks.Repo, []),
# Start the endpoint when the application starts
supervisor(GithubWebhooks.Endpoint, []),
# Start your own worker by calling: GithubWebhooks.Worker.start_link(arg1, arg2, arg3)
# worker(GithubWebhooks.Worker, [arg1, arg2, arg3]),
]

# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: GithubWebhooks.Supervisor]
Supervisor.start_link(children, opts)
end

# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
GithubWebhooks.Endpoint.config_change(changed, removed)
:ok
end
end
40 changes: 40 additions & 0 deletions lib/github_webhooks/endpoint.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule GithubWebhooks.Endpoint do
use Phoenix.Endpoint, otp_app: :github_webhooks

socket "/socket", GithubWebhooks.UserSocket

# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/", from: :github_webhooks, gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)

# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
plug Phoenix.CodeReloader
end

plug Plug.RequestId
plug Plug.Logger

plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Poison

plug Plug.MethodOverride
plug Plug.Head

# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug Plug.Session,
store: :cookie,
key: "_github_webhooks_key",
signing_salt: "/86cL0VL"

plug GithubWebhooks.Router
end
3 changes: 3 additions & 0 deletions lib/github_webhooks/repo.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule GithubWebhooks.Repo do
use Ecto.Repo, otp_app: :github_webhooks
end
52 changes: 52 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule GithubWebhooks.Mixfile do
use Mix.Project

def project do
[app: :github_webhooks,
version: "0.0.1",
elixir: "~> 1.2",
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix, :gettext] ++ Mix.compilers,
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
aliases: aliases(),
deps: deps()]
end

# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[mod: {GithubWebhooks, []},
applications: [:phoenix, :phoenix_pubsub, :cowboy, :logger, :gettext,
:phoenix_ecto, :postgrex]]
end

# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
defp elixirc_paths(_), do: ["lib", "web"]

# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[{:phoenix, "~> 1.2.0"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"}]
end

# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
"test": ["ecto.create --quiet", "ecto.migrate", "test"]]
end
end
16 changes: 16 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
%{"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []},
"cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
"db_connection": {:hex, :db_connection, "1.0.0-rc.4", "fad1f772c151cc6bde82412b8d72319968bc7221df8ef7d5e9d7fde7cb5c86b7", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0.0-beta.3", [hex: :sbroker, optional: true]}]},
"decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], []},
"ecto": {:hex, :ecto, "2.0.4", "03fd3b9aa508b1383eb38c00ac389953ed22af53811aa2e504975a3e814a8d97", [:mix], [{:db_connection, "~> 1.0-rc.2", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.11.2", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]},
"gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], []},
"mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []},
"phoenix": {:hex, :phoenix, "1.2.1", "6dc592249ab73c67575769765b66ad164ad25d83defa3492dc6ae269bd2a68ab", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.1", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]},
"phoenix_ecto": {:hex, :phoenix_ecto, "3.0.1", "42eb486ef732cf209d0a353e791806721f33ff40beab0a86f02070a5649ed00a", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.6", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.0", "c31af4be22afeeebfaf246592778c8c840e5a1ddc7ca87610c41ccfb160c2c57", [:mix], []},
"plug": {:hex, :plug, "1.2.0", "496bef96634a49d7803ab2671482f0c5ce9ce0b7b9bc25bc0ae8e09859dd2004", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]},
"poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []},
"postgrex": {:hex, :postgrex, "0.11.2", "139755c1359d3c5c6d6e8b1ea72556d39e2746f61c6ddfb442813c91f53487e8", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.0-rc", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]},
"ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []}}
Loading

0 comments on commit 554a598

Please sign in to comment.