import flask
import os
import hmac
import hashlib
import configparser
from ghia.common import GHIA, parse_rules
[docs]def load_config_web(app):
if GHIA.ENVVAR_CONFIG not in os.environ:
app.logger.critical(f'Config not supplied by envvar {GHIA.ENVVAR_CONFIG}')
exit(1)
config_files = os.environ[GHIA.ENVVAR_CONFIG].split(':')
cfg = configparser.ConfigParser()
cfg.optionxform = str
cfg.read(config_files)
if not cfg.has_option('github', 'token'):
app.logger.critical('Missing GitHub token in the given configuration')
exit(1)
app.config['github_token'] = cfg.get('github', 'token')
app.config['github_secret'] = cfg.get('github', 'secret', fallback=None)
try:
patterns, fallback_label = parse_rules(cfg)
app.config['patterns'] = patterns
app.config['fallback_label'] = fallback_label
except Exception:
app.logger.critical('Incorrect rules configuration format')
exit(1)
[docs]def webhook_verify_signature(payload, signature, secret, encoding='utf-8'):
"""
Verify the payload with given signature against given secret
see https://developer.github.com/webhooks/securing/
payload: received data as dict
signature: included SHA1 signature of payload (with secret)
secret: secret to verify signature
encoding: encoding for secret (optional)
"""
h = hmac.new(secret.encode(encoding), payload, hashlib.sha1)
return hmac.compare_digest('sha1=' + h.hexdigest(), signature)
GHIA.ISSUES_PROCESSED_ACTIONS = frozenset([
'opened', 'edited', 'transferred', 'reopened',
'assigned', 'unassigned', 'labeled', 'unlabeled'
])
[docs]def process_webhook_issues(payload):
"""
Process webhook event "issue"
payload: event payload
"""
ghia = flask.current_app.config['ghia']
reposlug = issue_number = ''
try:
action = payload['action']
issue = payload['issue']
issue_number = issue['number']
reposlug = payload['repository']['full_name']
owner, repo = reposlug.split('/')
if action not in ghia.ISSUES_PROCESSED_ACTIONS:
flask.current_app.logger.info(
f'Action {action} from {reposlug}#{issue_number} skipped'
)
return 'Accepted but action not processed', 202
if issue['state'] == 'open':
ghia.run_issue(owner, repo, issue)
flask.current_app.logger.info(
f'Action {action} from {reposlug}#{issue_number} processed'
)
return 'Issue successfully processed', 200
except (KeyError, IndexError):
flask.current_app.logger.info(
f'Incorrect data entity from IP {flask.request.remote_addr}'
)
flask.abort(422, 'Missing required payload fields')
except Exception:
flask.current_app.logger.error(
f'Error occurred while processing {reposlug}#{issue_number}'
)
flask.abort(500, 'Issue processing error')
[docs]def process_webhook_ping(payload):
"""
Process webhook event "ping"
payload: event payload
"""
try:
repo = payload['repository']['full_name']
hook_id = payload['hook_id']
flask.current_app.logger.info(
f'Accepting PING from {repo}#WH-{hook_id}'
)
return 'PONG', 200
except KeyError:
flask.current_app.logger.info(
f'Incorrect data entity from IP {flask.request.remote_addr}'
)
flask.abort(422, 'Missing payload contents')
webhook_processors = {
'issues': process_webhook_issues,
'ping': process_webhook_ping
}
ghia_blueprint = flask.Blueprint('ghia', __name__)
[docs]@ghia_blueprint.route('/', methods=['GET'])
def index():
return flask.render_template(
'index.html.j2',
rules=flask.current_app.config['patterns'],
fallback_label=flask.current_app.config['fallback_label'],
user=flask.current_app.config['github_user']
)
[docs]@ghia_blueprint.route('/', methods=['POST'])
def webhook_listener():
signature = flask.request.headers.get('X-Hub-Signature', '')
event = flask.request.headers.get('X-GitHub-Event', '')
payload = flask.request.get_json()
secret = flask.current_app.config['github_secret']
if secret is not None and not webhook_verify_signature(
flask.request.data, signature, secret
):
flask.current_app.logger.warning(
f'Attempt with bad secret from IP {flask.request.remote_addr}'
)
flask.abort(401, 'Bad webhook secret')
if event not in webhook_processors:
supported = ', '.join(webhook_processors.keys())
flask.abort(400, f'Event not supported (supported: {supported})')
return webhook_processors[event](payload)
[docs]def create_app(*args, **kwargs):
app = flask.Flask(__name__)
app.logger.info('Loading GHIA configuration from files')
load_config_web(app)
app.logger.info('Loading GHIA strategy and ry-run configuration')
strategy = os.environ.get(GHIA.ENVVAR_STRATEGY, GHIA.DEFAULT_STRATEGY)
if strategy not in GHIA.STRATEGIES.keys():
app.logger.critical(f'Unknown strategy "{strategy}" entered '
f'via envvar {GHIA.ENVVAR_STRATEGY}', err=True)
exit(1)
dry_run = configparser.ConfigParser.BOOLEAN_STATES.get(
os.environ.get(GHIA.ENVVAR_DRYRUN, '0').lower(), False
)
app.logger.info(f'Preparing GHIA with strategy {strategy} and dry-run '
f'{"enabled" if dry_run else "disabled"}')
app.config['ghia'] = GHIA(
app.config['github_token'],
app.config['patterns'],
app.config['fallback_label'],
dry_run,
strategy
)
# Possible to add some observer app.logger
try:
app.logger.info('Getting GitHub user using the given token')
app.config['github_user'] = app.config['ghia'].github.user()
except Exception:
app.logger.critical('Bad token: could not get GitHub user!', err=True)
exit(1)
app.register_blueprint(ghia_blueprint)
return app