-
Notifications
You must be signed in to change notification settings - Fork 336
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cd9db34
commit 771fac5
Showing
3 changed files
with
178 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
class WebhooksController < ApplicationController | ||
|
||
skip_before_action :verify_authenticity_token | ||
|
||
def create | ||
request_body = request.raw_post | ||
heroku_signature = request.headers['Heroku-Webhook-Hmac-SHA256'] | ||
secret = ENV['HEROKU_WEBHOOK_SECRET'] | ||
|
||
if secret.nil? | ||
render json: { message: 'Secret not set' }, status: :forbidden | ||
return | ||
end | ||
calculated_signature = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', secret, request_body)) | ||
|
||
if heroku_signature && secure_compare(calculated_signature, heroku_signature) | ||
webhook_url = ENV["RELEASE_WEBHOOK_URL"] | ||
current = params[:data][:current] | ||
status = params[:data][:status] | ||
build = params[:resource] == "build" && params[:action] == "create" && status == "pending" | ||
if webhook_url && (current || build) | ||
environment = ENV["HEROKU_APP_NAME"]&.include?("staging") ? "staging" : "production" | ||
notification_text = "New #{environment} Heroku event: " | ||
if build | ||
output = params[:data][:output_stream_url] | ||
notification_text += "Build started" | ||
details = "<#{output}|build log>" | ||
else | ||
notification_text += "#{params[:data][:description]} `#{status}`" | ||
commit_desc = params[:data][:slug][:commit_description] | ||
details = commit_desc | ||
end | ||
payload = { | ||
"mrkdwn": true, | ||
"text": notification_text, | ||
"blocks": [ | ||
{ | ||
"type": "section", | ||
"text": { | ||
"type": "mrkdwn", | ||
"text": "#{notification_text}\n#{details}", | ||
} | ||
} | ||
], | ||
"channel": "#software", | ||
}.to_json | ||
Faraday.post(webhook_url, | ||
payload, | ||
"Content-Type" => "application/json") | ||
end | ||
render json: { message: 'Webhook received successfully' }, status: :ok | ||
else | ||
render json: { message: 'Invalid signature' }, status: :forbidden | ||
end | ||
end | ||
|
||
private | ||
|
||
def secure_compare(calculated_signature, heroku_signature) | ||
ActiveSupport::SecurityUtils.secure_compare(calculated_signature, heroku_signature) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
require "spec_helper" | ||
|
||
describe WebhooksController do | ||
include Devise::Test::ControllerHelpers | ||
before do | ||
allow(Faraday).to receive(:post) | ||
end | ||
|
||
RELEASE = { | ||
id: "id", | ||
data: { | ||
status: "succeeded", | ||
current: true, | ||
description: "description", | ||
slug: { | ||
commit_description: "commit_description", | ||
}, | ||
}, | ||
resource: "release", | ||
action: "update", | ||
} | ||
BUILD = { | ||
id: "id", | ||
data: { | ||
status: "pending", | ||
output_stream_url: "output_stream_url", | ||
}, | ||
resource: "build", | ||
action: "create", | ||
} | ||
|
||
it "handles a release payload" do | ||
with_modified_env( | ||
RELEASE_WEBHOOK_URL: "https://example.com/webhook_url", | ||
HEROKU_WEBHOOK_SECRET: "secret", | ||
) do | ||
stub_request(:post, ENV["RELEASE_WEBHOOK_URL"]). | ||
to_return(status: 200, body: "", headers: {}) | ||
request.headers["Heroku-Webhook-Hmac-SHA256"] = Base64.strict_encode64( | ||
OpenSSL::HMAC.digest("sha256", "secret", RELEASE.to_json)) | ||
request.headers["Content-Type"] = "application/json" | ||
post :create, body: RELEASE.to_json, params: { format: :json } | ||
expect(response.status).to eq(200) | ||
expect(json[:message]).to eq("Webhook received successfully") | ||
expect(Faraday).to have_received(:post).with( | ||
ENV["RELEASE_WEBHOOK_URL"], | ||
{"mrkdwn":true, | ||
"text":"New production Heroku event: description `succeeded`", | ||
"blocks":[{ | ||
"type":"section", | ||
"text":{ | ||
"type":"mrkdwn", | ||
"text":"New production Heroku event: description `succeeded`\ncommit_description"}}], | ||
"channel":"#software"}.to_json, | ||
"Content-Type" => "application/json") | ||
end | ||
end | ||
|
||
it "handles a build payload" do | ||
with_modified_env( | ||
RELEASE_WEBHOOK_URL: "https://example.com/webhook_url", | ||
HEROKU_WEBHOOK_SECRET: "secret", | ||
) do | ||
stub_request(:post, ENV["RELEASE_WEBHOOK_URL"]). | ||
to_return(status: 200, body: "", headers: {}) | ||
request.headers["Heroku-Webhook-Hmac-SHA256"] = Base64.strict_encode64( | ||
OpenSSL::HMAC.digest("sha256", "secret", BUILD.to_json)) | ||
request.headers["Content-Type"] = "application/json" | ||
post :create, body: BUILD.to_json, params: { format: :json } | ||
expect(response.status).to eq(200) | ||
expect(json[:message]).to eq("Webhook received successfully") | ||
expect(Faraday).to have_received(:post).with( | ||
ENV["RELEASE_WEBHOOK_URL"], | ||
{"mrkdwn":true, | ||
"text":"New production Heroku event: Build started", | ||
"blocks":[{ | ||
"type":"section", | ||
"text":{ | ||
"type":"mrkdwn", | ||
"text":"New production Heroku event: Build started\n\u003coutput_stream_url|build log\u003e"}}], | ||
"channel":"#software"}.to_json, | ||
"Content-Type" => "application/json") | ||
end | ||
end | ||
|
||
it "receives a webhook: no relay" do | ||
with_modified_env(HEROKU_WEBHOOK_SECRET: "secret", RELEASE_WEBHOOK_URL: nil) do | ||
request.headers["Heroku-Webhook-Hmac-SHA256"] = Base64.strict_encode64( | ||
OpenSSL::HMAC.digest("sha256", "secret", RELEASE.to_json)) | ||
request.headers["Content-Type"] = "application/json" | ||
post :create, body: RELEASE.to_json, params: { format: :json } | ||
expect(response.status).to eq(200) | ||
expect(json[:message]).to eq("Webhook received successfully") | ||
end | ||
end | ||
|
||
it "rejects a webhook with an invalid signature" do | ||
with_modified_env HEROKU_WEBHOOK_SECRET: "secret" do | ||
request.headers["Heroku-Webhook-Hmac-SHA256"] = "invalid" | ||
request.headers["Content-Type"] = "application/json" | ||
post :create, body: RELEASE.to_json, params: { format: :json } | ||
expect(response.status).to eq(403) | ||
expect(json[:message]).to eq("Invalid signature") | ||
end | ||
end | ||
|
||
it "rejects a webhook with a missing secret" do | ||
with_modified_env HEROKU_WEBHOOK_SECRET: nil do | ||
request.headers["Content-Type"] = "application/json" | ||
post :create, body: RELEASE.to_json, params: { format: :json } | ||
expect(response.status).to eq(403) | ||
expect(json[:message]).to eq("Secret not set") | ||
end | ||
end | ||
end |