Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
source 'http://rubygems.org'

gem 'sinatra', '~> 2.0'
gem 'jwt', '~> 2.1'
gem 'octokit', '~> 4.0'
36 changes: 36 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
GEM
remote: http://rubygems.org/
specs:
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
faraday (0.15.2)
multipart-post (>= 1.2, < 3)
jwt (2.1.0)
multipart-post (2.0.0)
mustermann (1.0.2)
octokit (4.9.0)
sawyer (~> 0.8.0, >= 0.5.3)
public_suffix (3.0.2)
rack (2.0.5)
rack-protection (2.0.3)
rack
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
sinatra (2.0.3)
mustermann (~> 1.0)
rack (~> 2.0)
rack-protection (= 2.0.3)
tilt (~> 2.0)
tilt (2.0.8)

PLATFORMS
ruby

DEPENDENCIES
jwt (~> 2.1)
octokit (~> 4.0)
sinatra (~> 2.0)

BUNDLED WITH
1.14.6
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# charon
WIP: Github app that can do some useful PR checks

# Resources
- https://developer.github.com/apps/building-your-first-github-app/#start-the-server - tutorial I used to create that guy
160 changes: 160 additions & 0 deletions advanced_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
require 'sinatra'
require 'logger'
require 'json'
require 'openssl'
require 'octokit'
require 'jwt'
require 'time' # This is necessary to get the ISO 8601 representation of a Time object

set :port, 3000

#
#
# This is a customized server for the GitHub App you can build by following
# https://developer.github.com/build-your-first-github-app.
#
#

class GHAapp < Sinatra::Application

# Never, ever, hardcode app tokens or other secrets in your code!
# Always extract from a runtime source, like an environment variable.


# Notice that the private key must be in PEM format, but the newlines should be stripped and replaced with
# the literal `\n`. This can be done in the terminal as such:
# export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem`
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # convert newlines

# You set the webhook secret when you create your app. This verifies that the webhook is really coming from GH.
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']

# Get the app identifier—an integer—from your app page after you create your app. This isn't actually a secret,
# but it is something easier to configure at runtime.
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']

# You need to authenticate to the REST API to do much of anything

########## Configure Sinatra
#
# Let's turn on verbose logging during development
#
configure :development do
set :logging, Logger::DEBUG
end


########## Before each request to our app
#
# Before each request to our app, we want to instantiate an Octokit client. Doing so requires that we construct a JWT.
# https://jwt.io/introduction/
# We have to also sign that JWT with our private key, so GitHub can be sure that
# a) it came from us
# b) it hasn't been altered by a malicious third party
#
before do
payload = {
# The time that this JWT was issued, _i.e._ now.
iat: Time.now.to_i,

# How long is the JWT good for (in seconds)?
# Let's say it can be used for 10 minutes before it needs to be refreshed.
# TODO we don't actually cache this token, we regenerate a new one every time!
exp: Time.now.to_i + (10 * 60),

# Your GitHub App's identifier number, so GitHub knows who issued the JWT, and know what permissions
# this token has.
iss: APP_IDENTIFIER
}

# Cryptographically sign the JWT
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')

# Create the Octokit client, using the JWT as the auth token.
# Notice that this client will _not_ have sufficient permissions to do many interesting things!
# The helper methods below include one that generates an installation token (using the JWT) and
# instantiates a new client object.
@client ||= Octokit::Client.new(bearer_token: jwt)

end




########## Events
#
# This is the webhook endpoint that GH will call with events, and hence where we will do our event handling
#

post '/' do
request.body.rewind
payload_raw = request.body.read # We need the raw text of the body to check the webhook signature
begin
payload = JSON.parse payload_raw
rescue
payload = {}
end

# Check X-Hub-Signature to confirm that this webhook was generated by GitHub, and not a malicious third party.
# The way this works is: We have registered with GitHub a secret, and we have stored it locally in WEBHOOK_SECRET.
# GitHub will cryptographically sign the request payload with this secret. We will do the same, and if the results
# match, then we know that the request is from GitHub (or, at least, from someone who knows the secret!)
# If they don't match, this request is an attack, and we should reject it.
# The signature comes in with header x-hub-signature, and looks like "sha1=123456"
# We should take the left hand side as the signature method, and the right hand side as the
# HMAC digest (the signature) itself.
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
method, their_digest = their_signature_header.split('=')
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, payload_raw)
halt 401 unless their_digest == our_digest

# Determine what kind of event this is, and take action as appropriate
# TODO we assume that GitHub will always provide an X-GITHUB-EVENT header in this case, which is a reasonable
# assumption, however we should probably be more careful!
logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
logger.debug "---- action #{payload['action']}" unless payload['action'].nil?

case request.env['HTTP_X_GITHUB_EVENT']
when 'issues'
authenticate_installation(payload)
if payload['action'] === 'opened'
handle_issue_opened_event(payload)
end
end

'ok' # we have to return _something_ ;)
end


########## Helpers
#
# These functions are going to help us do some tasks that we don't want clogging up the happy paths above, or
# that need to be done repeatedly. You can add anything you like here, really!
#

helpers do

# Authenticate each installation of the app in order to run API operations
def authenticate_installation(payload)
installation_id = payload['installation']['id']
installation_token = @client.create_app_installation_access_token(installation_id)[:token]
@bot_client = Octokit::Client.new(bearer_token: installation_token)
end

# When an issue is opened, add a label
def handle_issue_opened_event(payload)
repo = payload['repository']['full_name']
issue_number = payload['issue']['number']
@bot_client.add_labels_to_an_issue(repo, issue_number, ['needs-response'])
end

end


# Finally some logic to let us run this server directly from the commandline, or with Rack
# Don't worry too much about this code ;) But, for the curious:
# $0 is the executed file
# __FILE__ is the current file
# If they are the same—that is, we are running this file directly, call the Sinatra run method
run! if __FILE__ == $0
end
2 changes: 2 additions & 0 deletions config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require "./server"
run GHAapp
Loading