Skip to content
This repository was archived by the owner on Jun 8, 2023. It is now read-only.

Commit 2a3c874

Browse files
committed
Update the template to move common code into helpers.
1 parent b4c5424 commit 2a3c874

File tree

6 files changed

+385
-1
lines changed

6 files changed

+385
-1
lines changed

Gemfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
source 'http://rubygems.org'
2+
3+
gem 'sinatra', '~> 2.0'
4+
gem 'jwt', '~> 2.1'
5+
gem 'octokit', '~> 4.0'

Gemfile.lock

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
GEM
2+
remote: http://rubygems.org/
3+
specs:
4+
addressable (2.5.2)
5+
public_suffix (>= 2.0.2, < 4.0)
6+
faraday (0.15.2)
7+
multipart-post (>= 1.2, < 3)
8+
jwt (2.1.0)
9+
multipart-post (2.0.0)
10+
mustermann (1.0.2)
11+
octokit (4.9.0)
12+
sawyer (~> 0.8.0, >= 0.5.3)
13+
public_suffix (3.0.2)
14+
rack (2.0.5)
15+
rack-protection (2.0.3)
16+
rack
17+
sawyer (0.8.1)
18+
addressable (>= 2.3.5, < 2.6)
19+
faraday (~> 0.8, < 1.0)
20+
sinatra (2.0.3)
21+
mustermann (~> 1.0)
22+
rack (~> 2.0)
23+
rack-protection (= 2.0.3)
24+
tilt (~> 2.0)
25+
tilt (2.0.8)
26+
27+
PLATFORMS
28+
ruby
29+
30+
DEPENDENCIES
31+
jwt (~> 2.1)
32+
octokit (~> 4.0)
33+
sinatra (~> 2.0)
34+
35+
BUNDLED WITH
36+
1.14.6

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,13 @@
1-
# building-your-first-github-app
1+
This is the sample project built by following the "[Building Your First GitHub App](https://developer.github.com/apps/building-your-first-github-app)" Quickstart guide on developer.github.com.
2+
3+
It consists of two different servers: `server.rb` (boilerplate) and `advanced_server.rb` (completed project).
4+
5+
## Install and run
6+
7+
To run the code, make sure you have [Bundler](http://gembundler.com/) installed; then enter `bundle install` on the command line.
8+
9+
* For the boilerplate project, enter `ruby server.rb` on the command line.
10+
11+
* For the completed project, enter `ruby advanced_server.rb` on the command line.
12+
13+
Both commands will run the server at `localhost:3000`.

advanced_server.rb

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
require 'sinatra'
2+
require 'octokit'
3+
require 'json'
4+
require 'openssl' # Used to verify the webhook signature
5+
require 'jwt' # Used to authenticate a GitHub App
6+
require 'time' # Used to get ISO 8601 representation of a Time object
7+
require 'logger'
8+
9+
set :port, 3000
10+
11+
12+
# This is template code to create a GitHub App server.
13+
# You can read more about GitHub Apps here: # https://developer.github.com/apps/
14+
#
15+
# On its own, this app does absolutely nothing, except that it can be installed.
16+
# It's up to you to add fun functionality!
17+
# You can check out one example in advanced_server.rb.
18+
#
19+
# This code is a Sinatra app, for two reasons:
20+
# 1. Because the app will require a landing page for installation.
21+
# 2. To easily handle webhook events.
22+
#
23+
#
24+
# Of course, not all apps need to receive and process events!
25+
# Feel free to rip out the event handling code if you don't need it.
26+
#
27+
# Have fun!
28+
#
29+
30+
class GHAapp < Sinatra::Application
31+
32+
# !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!!
33+
# Instead, set and read app tokens or other secrets in your code
34+
# in a runtime source, like an environment variable like below
35+
36+
# Expects that the private key has been set as an environment variable in
37+
# PEM format using the following command to replace newlines with the
38+
# literal `\n`:
39+
# export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem`
40+
#
41+
# Converts the newlines
42+
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))
43+
44+
# Your registered app must have a secret set. The secret is used to verify
45+
# that webhooks are sent by GitHub.
46+
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
47+
48+
# The GitHub App's identifier (type integer) set when registering an app.
49+
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
50+
51+
# Turn on Sinatra's verbose logging during development
52+
configure :development do
53+
set :logging, Logger::DEBUG
54+
end
55+
56+
57+
# Before each request to the `/event_handler` route
58+
before '/event_handler' do
59+
get_payload_request(request)
60+
verify_webhook_signature
61+
authenticate_app
62+
# Authenticate each installation of the app in order to run API operations
63+
authenticate_installation(@payload)
64+
end
65+
66+
67+
post '/event_handler' do
68+
69+
case request.env['HTTP_X_GITHUB_EVENT']
70+
when 'issues'
71+
if @payload['action'] === 'opened'
72+
handle_issue_opened_event(@payload)
73+
end
74+
end
75+
76+
'ok' # we have to return _something_ ;)
77+
78+
end
79+
80+
81+
helpers do
82+
83+
# When an issue is opened, add a label
84+
def handle_issue_opened_event(payload)
85+
repo = payload['repository']['full_name']
86+
issue_number = payload['issue']['number']
87+
@installation_client.add_labels_to_an_issue(repo, issue_number, ['needs-response'])
88+
end
89+
90+
# Saves the raw payload and converts the payload to JSON format
91+
def get_payload_request(request)
92+
# request.body is an IO or StringIO object
93+
# Rewind in case someone already read it
94+
request.body.rewind
95+
# The raw text of the body is required for webhook signature verification
96+
@payload_raw = request.body.read
97+
begin
98+
@payload = JSON.parse @payload_raw
99+
rescue => e
100+
fail "Invalid JSON (#{e}): #{@payload_raw}"
101+
end
102+
end
103+
104+
# Instantiate an Octokit client authenticated as a GitHub App.
105+
# GitHub App authentication equires that we construct a
106+
# JWT (https://jwt.io/introduction/) signed with the app's private key,
107+
# so GitHub can be sure that it came from the app an not altererd by
108+
# a malicious third party.
109+
def authenticate_app
110+
payload = {
111+
# The time that this JWT was issued, _i.e._ now.
112+
iat: Time.now.to_i,
113+
114+
# JWT expiration time (10 minute maximum)
115+
exp: Time.now.to_i + (10 * 60),
116+
117+
# Your GitHub App's identifier number
118+
iss: APP_IDENTIFIER
119+
}
120+
121+
# Cryptographically sign the JWT
122+
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
123+
124+
# Create the Octokit client, using the JWT as the auth token.
125+
@app_client ||= Octokit::Client.new(bearer_token: jwt)
126+
end
127+
128+
# Instantiate an Octokit client authenticated as an installation of a
129+
# GitHub App to run API operations.
130+
def authenticate_installation(payload)
131+
installation_id = payload['installation']['id']
132+
installation_token = @app_client.create_app_installation_access_token(installation_id)[:token]
133+
@installation_client = Octokit::Client.new(bearer_token: installation_token)
134+
end
135+
136+
# Check X-Hub-Signature to confirm that this webhook was generated by
137+
# GitHub, and not a malicious third party.
138+
#
139+
# GitHub will the WEBHOOK_SECRET, registered
140+
# to the GitHub App, to create a hash signature sent in each webhook payload
141+
# in the `X-HUB-Signature` header. This code computes the expected hash
142+
# signature and compares it to the signature sent in the `X-HUB-Signature`
143+
# header. If they don't match, this request is an attack, and we should
144+
# reject it. GitHub uses the HMAC hexdigest to compute the signature. The
145+
# `X-HUB-Signature` looks something like this: "sha1=123456"
146+
# See https://developer.github.com/webhooks/securing/ for details
147+
def verify_webhook_signature
148+
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
149+
method, their_digest = their_signature_header.split('=')
150+
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw)
151+
halt 401 unless their_digest == our_digest
152+
153+
# The X-GITHUB-EVENT header provides the name of the event.
154+
# The action value indicates the which action triggered the event.
155+
logger.debug "---- recevied event #{request.env['HTTP_X_GITHUB_EVENT']}"
156+
logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil?
157+
end
158+
159+
end
160+
161+
162+
# Finally some logic to let us run this server directly from the commandline, or with Rack
163+
# Don't worry too much about this code ;) But, for the curious:
164+
# $0 is the executed file
165+
# __FILE__ is the current file
166+
# If they are the same—that is, we are running this file directly, call the Sinatra run method
167+
run! if __FILE__ == $0
168+
end

config.ru

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
require "./server"
2+
run GHAapp

server.rb

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
require 'sinatra'
2+
require 'octokit'
3+
require 'json'
4+
require 'openssl' # Used to verify the webhook signature
5+
require 'jwt' # Used to authenticate a GitHub App
6+
require 'time' # Used to get ISO 8601 representation of a Time object
7+
require 'logger'
8+
9+
set :port, 3000
10+
11+
12+
# This is template code to create a GitHub App server.
13+
# You can read more about GitHub Apps here: # https://developer.github.com/apps/
14+
#
15+
# On its own, this app does absolutely nothing, except that it can be installed.
16+
# It's up to you to add fun functionality!
17+
# You can check out one example in advanced_server.rb.
18+
#
19+
# This code is a Sinatra app, for two reasons:
20+
# 1. Because the app will require a landing page for installation.
21+
# 2. To easily handle webhook events.
22+
#
23+
#
24+
# Of course, not all apps need to receive and process events!
25+
# Feel free to rip out the event handling code if you don't need it.
26+
#
27+
# Have fun!
28+
#
29+
30+
class GHAapp < Sinatra::Application
31+
32+
# !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!!
33+
# Instead, set and read app tokens or other secrets in your code
34+
# in a runtime source, like an environment variable like below
35+
36+
# Expects that the private key has been set as an environment variable in
37+
# PEM format using the following command to replace newlines with the
38+
# literal `\n`:
39+
# export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem`
40+
#
41+
# Converts the newlines
42+
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))
43+
44+
# Your registered app must have a secret set. The secret is used to verify
45+
# that webhooks are sent by GitHub.
46+
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
47+
48+
# The GitHub App's identifier (type integer) set when registering an app.
49+
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
50+
51+
# Turn on Sinatra's verbose logging during development
52+
configure :development do
53+
set :logging, Logger::DEBUG
54+
end
55+
56+
57+
# Before each request to the `/event_handler` route
58+
before '/event_handler' do
59+
get_payload_request(request)
60+
verify_webhook_signature
61+
authenticate_app
62+
# Authenticate each installation of the app in order to run API operations
63+
authenticate_installation(@payload)
64+
end
65+
66+
67+
post '/event_handler' do
68+
69+
# # # # # # # # # # # # # # # # # # #
70+
# ADD YOUR CODE HERE #
71+
# # # # # # # # # # # # # # # # # # #
72+
73+
'ok' # We've got to return _something_. ;)
74+
end
75+
76+
77+
helpers do
78+
79+
# # # # # # # # # # # # # # # # # # #
80+
# ADD YOUR HELPERS METHODS HERE #
81+
# # # # # # # # # # # # # # # # # # #
82+
83+
# Saves the raw payload and converts the payload to JSON format
84+
def get_payload_request(request)
85+
# request.body is an IO or StringIO object
86+
# Rewind in case someone already read it
87+
request.body.rewind
88+
# The raw text of the body is required for webhook signature verification
89+
@payload_raw = request.body.read
90+
begin
91+
@payload = JSON.parse @payload_raw
92+
rescue => e
93+
fail "Invalid JSON (#{e}): #{@payload_raw}"
94+
end
95+
end
96+
97+
# Instantiate an Octokit client authenticated as a GitHub App.
98+
# GitHub App authentication equires that we construct a
99+
# JWT (https://jwt.io/introduction/) signed with the app's private key,
100+
# so GitHub can be sure that it came from the app an not altererd by
101+
# a malicious third party.
102+
def authenticate_app
103+
payload = {
104+
# The time that this JWT was issued, _i.e._ now.
105+
iat: Time.now.to_i,
106+
107+
# JWT expiration time (10 minute maximum)
108+
exp: Time.now.to_i + (10 * 60),
109+
110+
# Your GitHub App's identifier number
111+
iss: APP_IDENTIFIER
112+
}
113+
114+
# Cryptographically sign the JWT
115+
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
116+
117+
# Create the Octokit client, using the JWT as the auth token.
118+
@app_client ||= Octokit::Client.new(bearer_token: jwt)
119+
end
120+
121+
# Instantiate an Octokit client authenticated as an installation of a
122+
# GitHub App to run API operations.
123+
def authenticate_installation(payload)
124+
installation_id = payload['installation']['id']
125+
installation_token = @app_client.create_app_installation_access_token(installation_id)[:token]
126+
@installation_client = Octokit::Client.new(bearer_token: installation_token)
127+
end
128+
129+
# Check X-Hub-Signature to confirm that this webhook was generated by
130+
# GitHub, and not a malicious third party.
131+
#
132+
# GitHub will the WEBHOOK_SECRET, registered
133+
# to the GitHub App, to create a hash signature sent in each webhook payload
134+
# in the `X-HUB-Signature` header. This code computes the expected hash
135+
# signature and compares it to the signature sent in the `X-HUB-Signature`
136+
# header. If they don't match, this request is an attack, and we should
137+
# reject it. GitHub uses the HMAC hexdigest to compute the signature. The
138+
# `X-HUB-Signature` looks something like this: "sha1=123456"
139+
# See https://developer.github.com/webhooks/securing/ for details
140+
def verify_webhook_signature
141+
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
142+
method, their_digest = their_signature_header.split('=')
143+
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw)
144+
halt 401 unless their_digest == our_digest
145+
146+
# The X-GITHUB-EVENT header provides the name of the event.
147+
# The action value indicates the which action triggered the event.
148+
logger.debug "---- recevied event #{request.env['HTTP_X_GITHUB_EVENT']}"
149+
logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil?
150+
end
151+
152+
end
153+
154+
155+
# Finally some logic to let us run this server directly from the commandline, or with Rack
156+
# Don't worry too much about this code ;) But, for the curious:
157+
# $0 is the executed file
158+
# __FILE__ is the current file
159+
# If they are the same—that is, we are running this file directly, call the Sinatra run method
160+
run! if __FILE__ == $0
161+
end

0 commit comments

Comments
 (0)