main
rb 56 lines 1.87 KB
Raw
1 # frozen_string_literal: true
2
3 # Receives GitHub push webhooks and triggers an immediate mirror sync for any
4 # mirror whose upstream matches the pushed repo. Optional: mirrors also sync on
5 # a schedule, so this is just a freshness optimization.
6 #
7 # Authenticated by the HMAC signature GitHub sends (X-Hub-Signature-256), keyed
8 # by GITHUB_WEBHOOK_SECRET. Without a configured secret the endpoint refuses
9 # requests rather than trusting unsigned input.
10 #
11 # Separate from GithubWebhooksController (the GitHub App's PR-review endpoint at
12 # /github/webhooks): different path, different secret, different trust domain.
13 class MirrorWebhooksController < ActionController::API
14 # POST /webhooks/github
15 def create
16 return head(:service_unavailable) if webhook_secret.blank?
17
18 payload = request.body.read
19 return head(:unauthorized) unless valid_signature?(payload)
20
21 event = request.headers["X-GitHub-Event"]
22 # Respond OK to ping so the webhook can be verified in GitHub's UI.
23 return head(:ok) if event == "ping"
24 return head(:ok) unless event == "push"
25
26 data = JSON.parse(payload) rescue {}
27 urls = candidate_urls(data)
28 return head(:ok) if urls.empty?
29
30 Repository.mirrors.where(upstream_url: urls).find_each do |repo|
31 MirrorSyncJob.perform_later(repo.id)
32 end
33
34 head :ok
35 end
36
37 private
38
39 # The clone URLs GitHub might have stored as our upstream for this repo.
40 def candidate_urls(data)
41 repo = data["repository"] || {}
42 [ repo["clone_url"], repo["git_url"], repo["html_url"], repo["ssh_url"] ].compact.uniq
43 end
44
45 def valid_signature?(payload)
46 sig = request.headers["X-Hub-Signature-256"].to_s
47 return false if sig.blank?
48
49 expected = "sha256=" + OpenSSL::HMAC.hexdigest("sha256", webhook_secret, payload)
50 ActiveSupport::SecurityUtils.secure_compare(sig, expected)
51 end
52
53 def webhook_secret
54 ENV["GITHUB_WEBHOOK_SECRET"].presence
55 end
56 end