Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GitHub webhook #1

Closed
wants to merge 1 commit into from
Closed
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
32 changes: 31 additions & 1 deletion roundup/cgi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from roundup.mailer import Mailer, MessageSendError, encode_quopri
from roundup.cgi import accept_language
from roundup import xmlrpc
from roundup.github_pullrequest_url import GitHubHandler

from roundup.anypy.cookie_ import CookieError, BaseCookie, SimpleCookie, \
get_cookie_date
Expand Down Expand Up @@ -378,13 +379,42 @@ def main(self):
try:
if self.path == 'xmlrpc':
self.handle_xmlrpc()
elif self.path == 'github_pullrequest_url':
self.handle_github_pullrequest_url()
else:
self.inner_main()
finally:
if hasattr(self, 'db'):
self.db.close()


def handle_github_pullrequest_url(self):
# Set the charset and language, since other parts of
# Roundup may depend upon that.
self.determine_charset()
self.determine_language()
# Open the database as the correct user.
self.determine_user()
self.check_anonymous_access()

try:
handler = GitHubHandler(self)
except Unauthorised, message:
self.response_code = 403
self.write(message)
except UnsupportedMediaType, message:
self.response_code = 415
self.write(message)
except MethodNotAllowed, message:
self.response_code = 405
self.write(message)
except Reject, message:
self.response_code = 400
self.write(message)
else:
self.write("Done!")


def handle_xmlrpc(self):
if self.env.get('CONTENT_TYPE') != 'text/xml':
self.write("This is the endpoint of Roundup <a href='" +
Expand Down Expand Up @@ -1165,7 +1195,7 @@ def selectTemplate(self, name, view):
if name is None:
name = 'home'

tplname = name
tplname = name
if view:
tplname = '%s.%s' % (name, view)

Expand Down
6 changes: 6 additions & 0 deletions roundup/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class Reject(Exception):
"""
pass

class UnsupportedMediaType(Exception):
pass

class MethodNotAllowed(Exception):
pass

class UsageError(ValueError):
pass

Expand Down
134 changes: 134 additions & 0 deletions roundup/github_pullrequest_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from roundup.exceptions import *

import hashlib
import hmac
import json
import re
import os


if hasattr(hmac, "compare_digest"):
compare_digest = hmac.compare_digest
else:
def compare_digest(a, b):
return a == b


class GitHubHandler:

def __init__(self, client):
self.db = client.db
self.request = client.request
self.form = client.form
self.data = json.loads(self.form.value)
self.env = client.env
self._validate_webhook_secret()
self._verify_request()
self._extract()

def _extract(self):
event = self._get_event()
if event not in ('pull_request', 'issue_comment'):
raise Reject('Unkown X-GitHub-Event %s' % event)
if event == 'pull_request':
PullRequest(self.db, self.data)
elif event == 'issue_comment':
IssueComment(self.db, self.data)

def _validate_webhook_secret(self):
key = os.environ['SECRET_KEY']
data = self.form.value
signature = "sha1=" + hmac.new(key, data,
hashlib.sha1).hexdigest()
header_signature = self.request.headers.get('X-Hub-Signature', '')
result = compare_digest(signature, header_signature)
if not result:
raise Unauthorised("The provided secret does not match")

def _verify_request(self):
method = self.env.get('REQUEST_METHOD', None)
if method != 'POST':
raise MethodNotAllowed('unsupported HTTP method %s' % method)
content_type = self.env.get('CONTENT_TYPE', None)
if content_type != 'application/json':
raise UnsupportedMediaType('unsupported Content-Type %s' %
content_type)
event = self._get_event()
if event is None:
raise Reject('missing X-GitHub-Event header')

def _get_event(self):
event = self.request.headers.get('X-GitHub-Event', None)
return event


class Event:

issue_re = re.compile(r'fixes\s+bpo(?P<id>\d+)', re.I)

def handle_create(self, url, issue_id):
issue_exists = len(self.db.issue.filter(None, {'id': issue_id})) == 1
url_exists = len(self.db.github_pullrequest_url
.filter(None, {'url': url})) == 1
if issue_exists and not url_exists:
url_id = self.db.github_pullrequest_url.create(url=url)
urls = self.db.issue.get(issue_id, 'github_pullrequest_urls')
urls.append(url_id)
self.db.issue.set(issue_id, github_pullrequest_urls=urls)
self.db.commit()

def _get_issue_id(self):
raise NotImplementedError

def _get_url(self):
raise NotImplementedError


class PullRequest(Event):

def __init__(self, db, data):
self.db = db
self.data = data
action = self.data['action'].encode('utf-8')
issue_id = self._get_issue_id()
url = self._get_url()
if action == 'opened':
self.handle_create(url, issue_id)

def _get_issue_id(self):
title = self.data['pull_request']['title'].encode('utf-8')
body = self.data['pull_request']['body'].encode('utf-8')
title_match = self.issue_re.search(title)
body_match = self.issue_re.search(body)
if body_match:
return body_match.group('id')
elif title_match:
return title_match.group('id')
return None

def _get_url(self):
return self.data['pull_request']['html_url'].encode('utf-8')


class IssueComment(Event):

def __init__(self, db, data):
self.db = db
self.data = data
action = self.data['action'].encode('utf-8')
issue_id = self._get_issue_id()
url = self._get_url()
if action == 'created':
self.handle_create(url, issue_id)

def _get_issue_id(self):
body = self.data['comment']['body'].encode('utf-8')
match = self.issue_re.search(body)
if match:
return match.group('id')
return None

def _get_url(self):
if 'pull_request' in self.data['issue']:
return self.data['issue']['pull_request']['html_url']\
.encode('utf-8')
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Auditor for GitHub URLs
# Check if it is a valid GitHub Pull Request URL and extract PR number
import re
import urlparse


def validate_github_url(db, cl, nodeid, newvalues):
url = newvalues.get('url', '')
parsed_url = urlparse.urlparse(url)
if parsed_url.scheme not in ('http', 'https'):
raise ValueError("Invalid URL scheme in GitHub Pull Request URL")
if 'github.com' not in parsed_url.netloc or 'pull' not in parsed_url.path:
raise ValueError("Invalid GitHub Pull Request URL")
newvalues['url'] = (parsed_url.scheme + "://" + parsed_url.netloc +
parsed_url.path)
regex = re.match(".*/pull/(\d+)", newvalues['url'])
if regex and len(regex.groups()) == 1:
pullrequest_number = regex.groups()[0]
try:
github_url_id = db.github_pullrequest_url.lookup(pullrequest_number)
raise ValueError("GitHub Pull Request URL already added to an issue")
except KeyError:
newvalues['pullrequest_number'] = pullrequest_number
else:
raise ValueError("Invalid GitHub Pull Request URL")


def init(db):
db.github_pullrequest_url.audit('create', validate_github_url)
db.github_pullrequest_url.audit('set', validate_github_url)
13 changes: 10 additions & 3 deletions share/roundup/templates/classic/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@
file = FileClass(db, "file",
name=String())

github_pullrequest_url = Class(db, "github_pullrequest_url",
url=String(),
pullrequest_number=String(),
)
github_pullrequest_url.setkey("pullrequest_number")

# IssueClass automatically gets these properties in addition to the Class ones:
# title = String()
# messages = Multilink("msg")
Expand All @@ -75,7 +81,8 @@
assignedto=Link("user"),
keyword=Multilink("keyword"),
priority=Link("priority"),
status=Link("status"))
status=Link("status"),
github_pullrequest_urls=Multilink('github_pullrequest_url'))

#
# TRACKER SECURITY SETTINGS
Expand All @@ -92,7 +99,7 @@

# Assign the access and edit Permissions for issue, file and message
# to regular users now
for cl in 'issue', 'file', 'msg', 'keyword':
for cl in 'issue', 'file', 'msg', 'keyword', 'github_pullrequest_url':
db.security.addPermissionToRole('User', 'View', cl)
db.security.addPermissionToRole('User', 'Edit', cl)
db.security.addPermissionToRole('User', 'Create', cl)
Expand Down Expand Up @@ -167,7 +174,7 @@ def edit_query(db, userid, itemid):

# Allow anonymous users access to view issues (and the related, linked
# information)
for cl in 'issue', 'file', 'msg', 'keyword', 'priority', 'status':
for cl in 'issue', 'file', 'msg', 'keyword', 'priority', 'status', 'github_pullrequest_url':
db.security.addPermissionToRole('Anonymous', 'View', cl)

# [OPTIONAL]
Expand Down
11 changes: 11 additions & 0 deletions test/data/issuecommentevent.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
POST /python-dev/github_pullrequest_url HTTP/1.1
Host: someurl.com
Accept: */*
User-Agent: GitHub-Hookshot/375c44e
X-GitHub-Event: issue_comment
X-GitHub-Delivery: a46d5700-20d9-11e6-87ed-62c21ca3b678
content-type: application/json
X-Hub-Signature: sha1=b7f8160e635a98c73d6995cea03bac8e5d11aa2e
Content-Length: 8648

{"action":"created","issue":{"url":"https://api.github.com/repos/AnishShah/cpython/issues/1","repository_url":"https://api.github.com/repos/AnishShah/cpython","labels_url":"https://api.github.com/repos/AnishShah/cpython/issues/1/labels{/name}","comments_url":"https://api.github.com/repos/AnishShah/cpython/issues/1/comments","events_url":"https://api.github.com/repos/AnishShah/cpython/issues/1/events","html_url":"https://github.com/AnishShah/cpython/pull/1","id":153285696,"number":1,"title":"test","user":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":1,"created_at":"2016-05-05T17:51:13Z","updated_at":"2016-05-23T11:29:42Z","closed_at":null,"pull_request":{"url":"https://api.github.com/repos/AnishShah/cpython/pulls/1","html_url":"https://github.com/AnishShah/cpython/pull/1","diff_url":"https://github.com/AnishShah/cpython/pull/1.diff","patch_url":"https://github.com/AnishShah/cpython/pull/1.patch"},"body":"Pls look at my PR. "},"comment":{"url":"https://api.github.com/repos/AnishShah/cpython/issues/comments/220955154","html_url":"https://github.com/AnishShah/cpython/pull/1#issuecomment-220955154","issue_url":"https://api.github.com/repos/AnishShah/cpython/issues/1","id":220955154,"user":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false},"created_at":"2016-05-23T11:29:42Z","updated_at":"2016-05-23T11:29:42Z","body":"fixes bpo1"},"repository":{"id":58147833,"name":"cpython","full_name":"AnishShah/cpython","owner":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/AnishShah/cpython","description":"Semi-official read-only mirror of the CPython Mercurial repository","fork":true,"url":"https://api.github.com/repos/AnishShah/cpython","forks_url":"https://api.github.com/repos/AnishShah/cpython/forks","keys_url":"https://api.github.com/repos/AnishShah/cpython/keys{/key_id}","collaborators_url":"https://api.github.com/repos/AnishShah/cpython/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/AnishShah/cpython/teams","hooks_url":"https://api.github.com/repos/AnishShah/cpython/hooks","issue_events_url":"https://api.github.com/repos/AnishShah/cpython/issues/events{/number}","events_url":"https://api.github.com/repos/AnishShah/cpython/events","assignees_url":"https://api.github.com/repos/AnishShah/cpython/assignees{/user}","branches_url":"https://api.github.com/repos/AnishShah/cpython/branches{/branch}","tags_url":"https://api.github.com/repos/AnishShah/cpython/tags","blobs_url":"https://api.github.com/repos/AnishShah/cpython/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/AnishShah/cpython/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/AnishShah/cpython/git/refs{/sha}","trees_url":"https://api.github.com/repos/AnishShah/cpython/git/trees{/sha}","statuses_url":"https://api.github.com/repos/AnishShah/cpython/statuses/{sha}","languages_url":"https://api.github.com/repos/AnishShah/cpython/languages","stargazers_url":"https://api.github.com/repos/AnishShah/cpython/stargazers","contributors_url":"https://api.github.com/repos/AnishShah/cpython/contributors","subscribers_url":"https://api.github.com/repos/AnishShah/cpython/subscribers","subscription_url":"https://api.github.com/repos/AnishShah/cpython/subscription","commits_url":"https://api.github.com/repos/AnishShah/cpython/commits{/sha}","git_commits_url":"https://api.github.com/repos/AnishShah/cpython/git/commits{/sha}","comments_url":"https://api.github.com/repos/AnishShah/cpython/comments{/number}","issue_comment_url":"https://api.github.com/repos/AnishShah/cpython/issues/comments{/number}","contents_url":"https://api.github.com/repos/AnishShah/cpython/contents/{+path}","compare_url":"https://api.github.com/repos/AnishShah/cpython/compare/{base}...{head}","merges_url":"https://api.github.com/repos/AnishShah/cpython/merges","archive_url":"https://api.github.com/repos/AnishShah/cpython/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/AnishShah/cpython/downloads","issues_url":"https://api.github.com/repos/AnishShah/cpython/issues{/number}","pulls_url":"https://api.github.com/repos/AnishShah/cpython/pulls{/number}","milestones_url":"https://api.github.com/repos/AnishShah/cpython/milestones{/number}","notifications_url":"https://api.github.com/repos/AnishShah/cpython/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/AnishShah/cpython/labels{/name}","releases_url":"https://api.github.com/repos/AnishShah/cpython/releases{/id}","deployments_url":"https://api.github.com/repos/AnishShah/cpython/deployments","created_at":"2016-05-05T17:13:56Z","updated_at":"2016-05-05T17:14:21Z","pushed_at":"2016-05-05T19:17:30Z","git_url":"git://github.com/AnishShah/cpython.git","ssh_url":"[email protected]:AnishShah/cpython.git","clone_url":"https://github.com/AnishShah/cpython.git","svn_url":"https://github.com/AnishShah/cpython","homepage":"","size":273763,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"},"sender":{"login":"AnishShah","id":3175743,"avatar_url":"https://avatars.githubusercontent.com/u/3175743?v=3","gravatar_id":"","url":"https://api.github.com/users/AnishShah","html_url":"https://github.com/AnishShah","followers_url":"https://api.github.com/users/AnishShah/followers","following_url":"https://api.github.com/users/AnishShah/following{/other_user}","gists_url":"https://api.github.com/users/AnishShah/gists{/gist_id}","starred_url":"https://api.github.com/users/AnishShah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AnishShah/subscriptions","organizations_url":"https://api.github.com/users/AnishShah/orgs","repos_url":"https://api.github.com/users/AnishShah/repos","events_url":"https://api.github.com/users/AnishShah/events{/privacy}","received_events_url":"https://api.github.com/users/AnishShah/received_events","type":"User","site_admin":false}}
Loading