Source code for githubissuesbot.web_app

from flask import Flask
from flask import render_template
from flask import request
import configparser
import hashlib
import hmac
import markdown
import appdirs
from socket import gethostname
import pkg_resources


def _read_web_config():
    """
    This function reads *web_config_file* and storing paths to other
    configuration files (auth.cfg, label.cfg and secret.cfg) and reading
    secret token value from the appropriate file.
    """
    global conf
    conf.read(web_config_file)

    global secret_file
    secret_file = conf['github']['secret_file']

    global auth_file
    auth_file = conf['github']['auth_file']

    global label_file
    label_file = conf['github']['label_file']

    # read file with a secret token for webhook
    conf.read(secret_file)


app_name = __name__.split('.')[0]
app = Flask(app_name)
conf = configparser.ConfigParser()

# if it is running on pythonanywhere.com (maybe that code works for other hosts)
if 'liveweb' in gethostname():
    import github_bot
    # set "web_config_file" variable to file with web configuration
    # format: /home/<username>/<project_name>/path/to/webcfg.cfg
    web_config_file = '/home/bobirdmi/MIPYTBotTMP/githubissuesbot/config/web.cfg'
    # read web configurations
    _read_web_config()
else:
    from . import github_bot


@app.route('/')
[docs]def index(): """ This function generates HTML formatted main page from README.md. """ readme_text = pkg_resources.resource_stream(app_name, 'README.md') return render_template('index.html', readme_text=readme_text.read().decode("utf-8"))
@app.route('/hook', methods=['POST'])
[docs]def hook(): """ The function handles labeling GitHub issues by receiving GitHub webhook POST requests. See https://developer.github.com/webhooks/ First, webhook signature (in header "X-Hub-Signature") is verified for security reasons as we don't want to handle undesirable requests. Then :py:func:`github_bot.GitHubBot` class is used for issue labeling. """ verify_signature(conf['github']['secret_token'], request.headers['X-Hub-Signature'], request.data) req_json = request.get_json() if req_json['action'] == 'opened': bot = github_bot.GitHubBot(auth_file, label_file, None, 'default') bot.label_issue(req_json['issue']) return str(req_json['issue']['url']) + ', ' + str(req_json['issue']['title']) + ', ' \ + str(req_json['issue']['body']) + ', ' + str(req_json['issue']['labels']) + ', ' \ + str(req_json['action'])
@app.template_filter('markdown')
[docs]def convert_markdown(text): """ Custom Flask template filter. Converts markdown text to safe html. Args: text (str): Markdown formatted text as Unicode or ASCII string. """ return markdown.markdown(text)
[docs]def verify_signature(secret: str, signature: str, resp_body) -> None: """ Verify HMAC-SHA1 signature of the given response body. The signature is expected to be in format ``sha1=<hex-digest>``. Args: secret (str): GitHub webhook secret token. See https://developer.github.com/webhooks/securing/ signature (str): SHA1 signature from webhook request (from header "X-Hub-Signature"). resp_body (str): Webhook request body. Raises: Exception: Error: signature is malformed. Exception: Error: expected type sha1. Exception: Error: digests do not match. .. testsetup:: from githubissuesbot import web_app fake_secret = '3ce92e588556bdh7220bc738b4dafad15bj7c196' body = 'this is my signature!'.encode('utf-8') good_signature = 'sha1=18fdf73c44d3a4b72b55b382c494f35a25b3a6e5' .. testcode:: :hide: web_app.verify_signature(fake_secret, good_signature, body) Verification of bad signature .. testcode:: bad_signature = 'sha1=18fdf73c44d3a4c72b55b382c494f35a25b3a6e5' web_app.verify_signature(fake_secret, bad_signature, body) .. testoutput:: Traceback (most recent call last): ... Exception: Error: digests do not match The following signature is broken: there is no '=' between hash type and signature itself .. testcode:: bad_signature2 = 'sha118fdf73c44d3a4c72b55b382c494f35a25b3a6e5' web_app.verify_signature(fake_secret, bad_signature2, body) .. testoutput:: Traceback (most recent call last): ... Exception: Error: signature is malformed The last signature is hashed by unsupported hash type (sha2) .. testcode:: bad_signature3 = 'sha2=18fdf73c44d3a4c72b55b382c494f35a25b3a6e5' web_app.verify_signature(fake_secret, bad_signature3, body) .. testoutput:: Traceback (most recent call last): ... Exception: Error: expected type sha1, but got sha2 """ try: alg, digest = signature.lower().split('=', 1) except (ValueError, AttributeError): raise Exception('Error: signature is malformed') if alg != 'sha1': raise Exception("Error: expected type sha1, but got %s" % alg) computed_digest = hmac.new(secret.encode('utf-8'), msg=resp_body, digestmod=hashlib.sha1).hexdigest() if not hmac.compare_digest(computed_digest, digest): raise Exception('Error: digests do not match')
[docs]def run_local_web(web_config): """ Runs local Flask server. Args: web_config (str): File with main web configuration. If None, the default file *web.cfg* will be used. """ global web_config_file if web_config: web_config_file = web_config else: web_config_file = appdirs.site_config_dir(appname=app_name) + '/web.cfg' # read web configuration _read_web_config() app.config['TEMPLATES_AUTO_RELOAD'] = True app.run(debug=True)