├── .bumpversion.cfg ├── .coveragerc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── authors.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── index.rst ├── installation.rst ├── readme.rst ├── reference │ ├── django-webhook-github.rst │ └── index.rst ├── requirements.txt ├── spelling_wordlist.txt └── usage.rst ├── manage.py ├── setup.cfg ├── setup.py ├── src └── django_github_webhook │ ├── __init__.py │ └── views.py ├── tests ├── __init__.py ├── settings.py ├── test_django-webhook-github.py └── urls.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.1 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:docs/conf.py] 9 | 10 | [bumpversion:file:src/django_github_webhook/__init__.py] 11 | 12 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = 3 | src/django_github_webhook 4 | */site-packages/django_github_webhook 5 | 6 | [run] 7 | branch = True 8 | source = django-github-webhook 9 | parallel = true 10 | 11 | [report] 12 | show_missing = true 13 | precision = 2 14 | omit = *migrations* 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | 12 | [*.{bat,cmd,ps1}] 13 | end_of_line = crlf 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | .eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | venv*/ 22 | pyvenv*/ 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | .coverage.* 31 | nosetests.xml 32 | coverage.xml 33 | htmlcov 34 | 35 | # Translations 36 | *.mo 37 | 38 | # Mr Developer 39 | .mr.developer.cfg 40 | .project 41 | .pydevproject 42 | .idea 43 | *.iml 44 | *.komodoproject 45 | 46 | # Complexity 47 | output/*.html 48 | output/*/index.html 49 | 50 | # Sphinx 51 | docs/_build 52 | 53 | .DS_Store 54 | *~ 55 | .*.sw[po] 56 | .build 57 | .ve 58 | .env 59 | .cache 60 | .pytest 61 | .bootstrap 62 | .appveyor.token 63 | *.bak 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: '3.5' 3 | sudo: false 4 | env: 5 | global: 6 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | - SEGFAULT_SIGNALS=all 8 | matrix: 9 | - TOXENV=check 10 | - TOXENV=docs 11 | 12 | - TOXENV=py34,codecov 13 | - TOXENV=py35,codecov 14 | - TOXENV=pypy,codecov 15 | before_install: 16 | - python --version 17 | - uname -a 18 | - lsb_release -a 19 | install: 20 | - pip install tox 21 | - virtualenv --version 22 | - easy_install --version 23 | - pip --version 24 | - tox --version 25 | script: 26 | - tox -v 27 | after_failure: 28 | - more .tox/log/* | cat 29 | - more .tox/*/log/* | cat 30 | before_cache: 31 | - rm -rf $HOME/.cache/pip/log 32 | cache: 33 | directories: 34 | - $HOME/.cache/pip 35 | notifications: 36 | email: 37 | on_success: never 38 | on_failure: always 39 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | 2 | Authors 3 | ======= 4 | 5 | * Michael Fladischer - https://openservices.at 6 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | Changelog 3 | ========= 4 | 5 | 0.1.0 (2016-01-29) 6 | ----------------------------------------- 7 | 8 | * First release on PyPI. 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Bug reports 9 | =========== 10 | 11 | When `reporting a bug `_ please include: 12 | 13 | * Your operating system name and version. 14 | * Any details about your local setup that might be helpful in troubleshooting. 15 | * Detailed steps to reproduce the bug. 16 | 17 | Documentation improvements 18 | ========================== 19 | 20 | django-github-webhook could always use more documentation, whether as part of the 21 | official django-github-webhook docs, in docstrings, or even on the web in blog posts, 22 | articles, and such. 23 | 24 | Feature requests and feedback 25 | ============================= 26 | 27 | The best way to send feedback is to file an issue at https://github.com/fladi/django-github-webhook/issues. 28 | 29 | If you are proposing a feature: 30 | 31 | * Explain in detail how it would work. 32 | * Keep the scope as narrow as possible, to make it easier to implement. 33 | * Remember that this is a volunteer-driven project, and that code contributions are welcome :) 34 | 35 | Development 36 | =========== 37 | 38 | To set up `django-github-webhook` for local development: 39 | 40 | 1. Fork `django-github-webhook `_ 41 | (look for the "Fork" button). 42 | 2. Clone your fork locally:: 43 | 44 | git clone git@github.com:your_name_here/django-github-webhook.git 45 | 46 | 3. Create a branch for local development:: 47 | 48 | git checkout -b name-of-your-bugfix-or-feature 49 | 50 | Now you can make your changes locally. 51 | 52 | 4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: 53 | 54 | tox 55 | 56 | 5. Commit your changes and push your branch to GitHub:: 57 | 58 | git add . 59 | git commit -m "Your detailed description of your changes." 60 | git push origin name-of-your-bugfix-or-feature 61 | 62 | 6. Submit a pull request through the GitHub website. 63 | 64 | Pull Request Guidelines 65 | ----------------------- 66 | 67 | If you need some code review or feedback while you're developing the code just make the pull request. 68 | 69 | For merging, you should: 70 | 71 | 1. Include passing tests (run ``tox``) [1]_. 72 | 2. Update documentation when there's new API, functionality etc. 73 | 3. Add a note to ``CHANGELOG.rst`` about the changes. 74 | 4. Add yourself to ``AUTHORS.rst``. 75 | 76 | .. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will 77 | `run the tests `_ for each change you add in the pull request. 78 | 79 | It will be slower though ... 80 | 81 | Tips 82 | ---- 83 | 84 | To run a subset of tests:: 85 | 86 | tox -e envname -- py.test -k test_myfeature 87 | 88 | To run all the test environments in *parallel* (you need to ``pip install detox``):: 89 | 90 | detox 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Michael Fladischer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 8 | disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 16 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 17 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 18 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 19 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft src 3 | graft tests 4 | 5 | include .bumpversion.cfg 6 | include .coveragerc 7 | include .editorconfig 8 | 9 | include AUTHORS.rst 10 | include CHANGELOG.rst 11 | include CONTRIBUTING.rst 12 | include LICENSE 13 | include README.rst 14 | 15 | include tox.ini .travis.yml 16 | include manage.py 17 | 18 | global-exclude *.py[cod] __pycache__ *.so *.dylib 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Overview 3 | ======== 4 | 5 | .. start-badges 6 | 7 | .. list-table:: 8 | :stub-columns: 1 9 | 10 | * - docs 11 | - |docs| 12 | * - tests 13 | - | |travis| |requires| 14 | | |codecov| 15 | * - package 16 | - |version| |downloads| |wheel| |supported-versions| |supported-implementations| 17 | 18 | .. |docs| image:: https://readthedocs.org/projects/django-github-webhook/badge/?style=flat 19 | :target: https://readthedocs.org/projects/django-github-webhook 20 | :alt: Documentation Status 21 | 22 | .. |travis| image:: https://travis-ci.org/fladi/django-github-webhook.svg?branch=master 23 | :alt: Travis-CI Build Status 24 | :target: https://travis-ci.org/fladi/django-github-webhook 25 | 26 | .. |requires| image:: https://requires.io/github/fladi/django-github-webhook/requirements.svg?branch=master 27 | :alt: Requirements Status 28 | :target: https://requires.io/github/fladi/django-github-webhook/requirements/?branch=master 29 | 30 | .. |codecov| image:: https://codecov.io/github/fladi/django-github-webhook/coverage.svg?branch=master 31 | :alt: Coverage Status 32 | :target: https://codecov.io/github/fladi/django-github-webhook 33 | 34 | .. |version| image:: https://img.shields.io/pypi/v/django-github-webhook.svg?style=flat 35 | :alt: PyPI Package latest release 36 | :target: https://pypi.python.org/pypi/django-github-webhook 37 | 38 | .. |downloads| image:: https://img.shields.io/pypi/dm/django-github-webhook.svg?style=flat 39 | :alt: PyPI Package monthly downloads 40 | :target: https://pypi.python.org/pypi/django-github-webhook 41 | 42 | .. |wheel| image:: https://img.shields.io/pypi/wheel/django-github-webhook.svg?style=flat 43 | :alt: PyPI Wheel 44 | :target: https://pypi.python.org/pypi/django-github-webhook 45 | 46 | .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/django-github-webhook.svg?style=flat 47 | :alt: Supported versions 48 | :target: https://pypi.python.org/pypi/django-github-webhook 49 | 50 | .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/django-github-webhook.svg?style=flat 51 | :alt: Supported implementations 52 | :target: https://pypi.python.org/pypi/django-github-webhook 53 | 54 | 55 | .. end-badges 56 | 57 | A class based view for Django that can act as an receiver for GitHub webhooks. It is designed to validate all requests through their ``X-Hub-Signature`` 58 | headers. 59 | 60 | Handling of GitHub events is done by implementing a class method with the same name as the event, e.g. ``ping``, ``push`` or ``fork``. See the documentation for 61 | more in-depth information and examples. 62 | 63 | * Free software: BSD license 64 | 65 | Installation 66 | ============ 67 | 68 | :: 69 | 70 | pip install django-github-webhook 71 | 72 | Documentation 73 | ============= 74 | 75 | https://django-github-webhook.readthedocs.org/ 76 | 77 | Development 78 | =========== 79 | 80 | To run the all tests run:: 81 | 82 | tox 83 | 84 | Note, to combine the coverage data from all the tox environments run: 85 | 86 | .. list-table:: 87 | :widths: 10 90 88 | :stub-columns: 1 89 | 90 | - - Windows 91 | - :: 92 | 93 | set PYTEST_ADDOPTS=--cov-append 94 | tox 95 | 96 | - - Other 97 | - :: 98 | 99 | PYTEST_ADDOPTS=--cov-append tox 100 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | 6 | 7 | extensions = [ 8 | 'sphinx.ext.autodoc', 9 | 'sphinx.ext.autosummary', 10 | 'sphinx.ext.coverage', 11 | 'sphinx.ext.doctest', 12 | 'sphinx.ext.extlinks', 13 | 'sphinx.ext.ifconfig', 14 | 'sphinx.ext.napoleon', 15 | 'sphinx.ext.todo', 16 | 'sphinx.ext.viewcode', 17 | ] 18 | if os.getenv('SPELLCHECK'): 19 | extensions += 'sphinxcontrib.spelling', 20 | spelling_show_suggestions = True 21 | spelling_lang = 'en_US' 22 | 23 | source_suffix = '.rst' 24 | master_doc = 'index' 25 | project = 'django-github-webhook' 26 | year = '2016' 27 | author = 'Michael Fladischer' 28 | copyright = '{0}, {1}'.format(year, author) 29 | version = release = '0.1.1' 30 | 31 | pygments_style = 'trac' 32 | templates_path = ['.'] 33 | extlinks = { 34 | 'issue': ('https://github.com/fladi/django-github-webhook/issues/%s', '#'), 35 | 'pr': ('https://github.com/fladi/django-github-webhook/pull/%s', 'PR #'), 36 | } 37 | # on_rtd is whether we are on readthedocs.org 38 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 39 | 40 | if not on_rtd: # only import and set the theme if we're building docs locally 41 | import sphinx_rtd_theme 42 | html_theme = 'sphinx_rtd_theme' 43 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 44 | 45 | html_use_smartypants = True 46 | html_last_updated_fmt = '%b %d, %Y' 47 | html_split_index = True 48 | html_sidebars = { 49 | '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], 50 | } 51 | html_short_title = '%s-%s' % (project, version) 52 | 53 | napoleon_use_ivar = True 54 | napoleon_use_rtype = False 55 | napoleon_use_param = False 56 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Contents 3 | ======== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | readme 9 | installation 10 | usage 11 | reference/index 12 | contributing 13 | authors 14 | changelog 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | pip install django-github-webhook 8 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/reference/django-webhook-github.rst: -------------------------------------------------------------------------------- 1 | django_webhook_github 2 | ===================== 3 | 4 | .. testsetup:: 5 | 6 | from django_webhook_github import * 7 | 8 | .. automodule:: django_webhook_github 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | django_webhook_github* 8 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.3 2 | -e . 3 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | builtin 2 | builtins 3 | classmethod 4 | staticmethod 5 | classmethods 6 | staticmethods 7 | args 8 | kwargs 9 | callstack 10 | Changelog 11 | Indices 12 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | .. _GitHub events: https://developer.github.com/v3/activity/events/types/ 6 | 7 | To use django-github-webhook in a project where you want to receive webhooks for ``push`` events:: 8 | 9 | from django_github_webhook.views import WebHookView 10 | 11 | 12 | class MyWebHookReceiverView(WebHookView): 13 | secret = 'foobar' 14 | 15 | def push(self, payload, request): 16 | ''' Do something with the payload and return a JSON serializeable value. ''' 17 | return {'status': 'received'} 18 | 19 | 20 | If the secret has to be dynamically fetched for each request you should override the ``get_secret`` method:: 21 | 22 | from .models import Hook 23 | 24 | class MyWebHookReceiverView(WebHookView): 25 | 26 | def get_secret(self): 27 | hook = Hook.objects.get(pk=self.request.kwargs['id']) 28 | return hook.secret 29 | 30 | Each webhook can receive multiple `GitHub events`_ by implementing methods with the same name as the events. Right now the following events are accepted: 31 | 32 | * commit_comment 33 | * create 34 | * delete 35 | * deployment 36 | * deployment_status 37 | * fork 38 | * gollum 39 | * issue_comment 40 | * issues 41 | * member 42 | * membership 43 | * page_build 44 | * ping 45 | * public 46 | * pull_request 47 | * pull_request_review_comment 48 | * push 49 | * release 50 | * repository 51 | * status 52 | * team_add 53 | * watch 54 | 55 | So in order to accept events of type ``fork`` and ``watch`` implement methods as follows. The ``payload`` parameter gets the already decoded JSON payload from 56 | the request body:: 57 | 58 | class MyWebHookReceiverView(WebHookView): 59 | 60 | def fork(self, payload, request): 61 | print('Forked by {payload[forkee][full_name]}'.format(payload=payload)) 62 | return {'status': 'forked'} 63 | 64 | def watch(self, payload, request): 65 | print('Watched by {payload[sender][login]}'.format(payload=payload)) 66 | 67 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fladi/django-github-webhook/772242b4d89d3e85c246bab31c6f0039a32ccdef/manage.py -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [aliases] 5 | release = register clean --all sdist bdist_wheel 6 | 7 | [flake8] 8 | max-line-length = 140 9 | exclude = tests/*,*/migrations/*,*/south_migrations/* 10 | 11 | [pytest] 12 | norecursedirs = 13 | .git 14 | .tox 15 | .env 16 | dist 17 | build 18 | south_migrations 19 | migrations 20 | python_files = 21 | test_*.py 22 | *_test.py 23 | tests.py 24 | addopts = 25 | -rxEfsw 26 | --strict 27 | --doctest-modules 28 | --doctest-glob=\*.rst 29 | --tb=short 30 | --ignore=setup.py 31 | DJANGO_SETTINGS_MODULE=tests.settings 32 | 33 | [isort] 34 | force_single_line=True 35 | line_length=120 36 | known_first_party=django_webhook_github 37 | default_section=THIRDPARTY 38 | forced_separate=test_django_webhook_github 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | from __future__ import absolute_import, print_function 4 | 5 | import io 6 | import re 7 | from glob import glob 8 | from os.path import basename 9 | from os.path import dirname 10 | from os.path import join 11 | from os.path import splitext 12 | 13 | from setuptools import find_packages 14 | from setuptools import setup 15 | 16 | 17 | def read(*names, **kwargs): 18 | return io.open( 19 | join(dirname(__file__), *names), 20 | encoding=kwargs.get('encoding', 'utf8') 21 | ).read() 22 | 23 | 24 | setup( 25 | name='django-github-webhook', 26 | version='0.1.1', 27 | license='BSD', 28 | description='Django view for GitHub webhook recievers', 29 | long_description='%s\n%s' % ( 30 | re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), 31 | re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) 32 | ), 33 | author='Michael Fladischer', 34 | author_email='michael@openservices.at', 35 | url='https://github.com/fladi/django-github-webhook', 36 | packages=find_packages('src'), 37 | package_dir={'': 'src'}, 38 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 39 | include_package_data=True, 40 | zip_safe=False, 41 | classifiers=[ 42 | 'Development Status :: 5 - Production/Stable', 43 | 'Intended Audience :: Developers', 44 | 'License :: OSI Approved :: BSD License', 45 | 'Programming Language :: Python', 46 | 'Programming Language :: Python :: 3', 47 | 'Programming Language :: Python :: 3.4', 48 | 'Programming Language :: Python :: 3.5', 49 | 'Programming Language :: Python :: Implementation :: CPython', 50 | 'Programming Language :: Python :: Implementation :: PyPy', 51 | 'Topic :: Utilities', 52 | ], 53 | keywords=[ 54 | 'django', 'view', 'github', 'webhook', 'signature' 55 | ], 56 | install_requires=[ 57 | 'Django' 58 | ], 59 | extras_require={ 60 | }, 61 | ) 62 | -------------------------------------------------------------------------------- /src/django_github_webhook/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.1" 2 | -------------------------------------------------------------------------------- /src/django_github_webhook/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import hashlib 4 | import hmac 5 | import json 6 | 7 | from django.utils.decorators import method_decorator 8 | from django.views.decorators.csrf import csrf_exempt 9 | from django.core.exceptions import ImproperlyConfigured 10 | from django.http import HttpResponseBadRequest, JsonResponse 11 | from django.views.generic import View 12 | 13 | 14 | class WebHookView(View): 15 | secret = None 16 | allowed_events = [ 17 | 'commit_comment', 18 | 'create', 19 | 'delete', 20 | 'deployment', 21 | 'deployment_status', 22 | 'fork', 23 | 'gollum', 24 | 'issue_comment', 25 | 'issues', 26 | 'member', 27 | 'membership', 28 | 'page_build', 29 | 'ping', 30 | 'public', 31 | 'pull_request', 32 | 'pull_request_review_comment', 33 | 'push', 34 | 'release', 35 | 'repository', 36 | 'status', 37 | 'team_add', 38 | 'watch', 39 | ] 40 | 41 | def get_secret(self): 42 | return self.secret 43 | 44 | def get_allowed_events(self): 45 | return self.allowed_events 46 | 47 | @method_decorator(csrf_exempt) 48 | def dispatch(self, request, *args, **kwargs): 49 | return super(WebHookView, self).dispatch(request, *args, **kwargs) 50 | 51 | def post(self, request, *args, **kwargs): 52 | secret = self.get_secret() 53 | if not secret: 54 | raise ImproperlyConfigured('GitHub webhook secret ist not defined.') 55 | if 'HTTP_X_HUB_SIGNATURE' not in request.META: 56 | return HttpResponseBadRequest('Request does not contain X-GITHUB-SIGNATURE header') 57 | if 'HTTP_X_GITHUB_EVENT' not in request.META: 58 | return HttpResponseBadRequest('Request does not contain X-GITHUB-EVENT header') 59 | digest_name, signature = request.META['HTTP_X_HUB_SIGNATURE'].split('=') 60 | if digest_name != 'sha1': 61 | return HttpResponseBadRequest('Unsupported X-HUB-SIGNATURE digest mode found: {}'.format(digest_name)) 62 | mac = hmac.new( 63 | secret.encode('utf-8'), 64 | msg=request.body, 65 | digestmod=hashlib.sha1 66 | ) 67 | if not hmac.compare_digest(mac.hexdigest(), signature): 68 | return HttpResponseBadRequest('Invalid X-HUB-SIGNATURE header found') 69 | event = request.META['HTTP_X_GITHUB_EVENT'] 70 | if event not in self.get_allowed_events(): 71 | return HttpResponseBadRequest('Unsupported X-GITHUB-EVENT header found: {}'.format(event)) 72 | handler = getattr(self, event, None) 73 | if not handler: 74 | return HttpResponseBadRequest('Unsupported X-GITHUB-EVENT header found: {}'.format(event)) 75 | payload = json.loads(request.body.decode('utf-8')) 76 | response = handler(payload, request, *args, **kwargs) 77 | return JsonResponse(response) 78 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fladi/django-github-webhook/772242b4d89d3e85c246bab31c6f0039a32ccdef/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | SECRET_KEY = 'an9)lb_asn(6ri6++ic_fat5%e&6y_t*0l9u&a27!bald=gflw' 4 | 5 | ROOT_URLCONF = 'tests.urls' 6 | -------------------------------------------------------------------------------- /tests/test_django-webhook-github.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import hashlib 4 | import hmac 5 | import json 6 | import unittest 7 | 8 | from django.core.exceptions import ImproperlyConfigured 9 | from django.http import ( 10 | HttpResponseNotAllowed, 11 | HttpResponseBadRequest, 12 | JsonResponse 13 | ) 14 | from django.test import RequestFactory 15 | 16 | from django_github_webhook.views import WebHookView 17 | 18 | 19 | class TestNoSecretView(unittest.TestCase): 20 | 21 | def setUp(self): 22 | class NoSecretView(WebHookView): 23 | pass 24 | 25 | self.factory = RequestFactory() 26 | self.view = NoSecretView.as_view() 27 | 28 | def test_raises_exception(self): 29 | #import pudb; pu.db 30 | request = self.factory.post('/fake') 31 | with self.assertRaises(ImproperlyConfigured): 32 | self.view(request) 33 | 34 | 35 | class TestSimpleView(unittest.TestCase): 36 | 37 | def setUp(self): 38 | class SimpleView(WebHookView): 39 | secret = 'foobar' 40 | 41 | def ping(self, payload, request): 42 | return payload 43 | 44 | self.factory = RequestFactory() 45 | self.view = SimpleView.as_view() 46 | self.payload = json.dumps({'success': True}) 47 | self.mac_valid = hmac.new( 48 | self.view.view_class.secret.encode('utf-8'), 49 | msg=self.payload.encode('utf-8'), 50 | digestmod=hashlib.sha1 51 | ) 52 | self.mac_invalid = hmac.new( 53 | 'malicious'.encode('utf-8'), 54 | msg=self.payload.encode('utf-8'), 55 | digestmod=hashlib.sha1 56 | ) 57 | 58 | def test_get(self): 59 | request = self.factory.get('/fake') 60 | response = self.view(request) 61 | self.assertIsInstance(response, HttpResponseNotAllowed) 62 | 63 | def test_post(self): 64 | request = self.factory.post('/fake') 65 | response = self.view(request) 66 | self.assertIsInstance(response, HttpResponseBadRequest) 67 | 68 | def test_valid_signature(self): 69 | request = self.factory.post( 70 | '/fake', 71 | self.payload, 72 | content_type='application/json', 73 | HTTP_X_GITHUB_EVENT='ping', 74 | HTTP_X_HUB_SIGNATURE='sha1={mac}'.format(mac=self.mac_valid.hexdigest()) 75 | ) 76 | response = self.view(request) 77 | self.assertIsInstance(response, JsonResponse) 78 | 79 | def test_invalid_signature(self): 80 | request = self.factory.post( 81 | '/fake', 82 | self.payload, 83 | content_type='application/json', 84 | HTTP_X_GITHUB_EVENT='ping', 85 | HTTP_X_HUB_SIGNATURE='sha1={mac}'.format(mac=self.mac_invalid.hexdigest()) 86 | ) 87 | response = self.view(request) 88 | self.assertIsInstance(response, HttpResponseBadRequest) 89 | 90 | def test_no_event(self): 91 | request = self.factory.post( 92 | '/fake', 93 | self.payload, 94 | content_type='application/json', 95 | HTTP_X_HUB_SIGNATURE='sha1=x' 96 | ) 97 | response = self.view(request) 98 | self.assertIsInstance(response, HttpResponseBadRequest) 99 | 100 | def test_invalid_digest(self): 101 | request = self.factory.post( 102 | '/fake', 103 | self.payload, 104 | content_type='application/json', 105 | HTTP_X_GITHUB_EVENT='ping', 106 | HTTP_X_HUB_SIGNATURE='md5=x' 107 | ) 108 | response = self.view(request) 109 | self.assertIsInstance(response, HttpResponseBadRequest) 110 | 111 | def test_event_not_allowed(self): 112 | request = self.factory.post( 113 | '/fake', 114 | self.payload, 115 | content_type='application/json', 116 | HTTP_X_GITHUB_EVENT='merge', 117 | HTTP_X_HUB_SIGNATURE='sha1={mac}'.format(mac=self.mac_valid.hexdigest()) 118 | ) 119 | response = self.view(request) 120 | self.assertIsInstance(response, HttpResponseBadRequest) 121 | 122 | def test_event_not_supported(self): 123 | request = self.factory.post( 124 | '/fake', 125 | self.payload, 126 | content_type='application/json', 127 | HTTP_X_GITHUB_EVENT='push', 128 | HTTP_X_HUB_SIGNATURE='sha1={mac}'.format(mac=self.mac_valid.hexdigest()) 129 | ) 130 | response = self.view(request) 131 | self.assertIsInstance(response, HttpResponseBadRequest) 132 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | urlpatterns = [ 4 | ] 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ; a generative tox configuration, see: https://testrun.org/tox/latest/config.html#generative-envlist 2 | 3 | [tox] 4 | envlist = 5 | clean, 6 | check, 7 | {py34,py35,py36,py37,pypy}, 8 | report, 9 | docs 10 | 11 | [testenv] 12 | basepython = 13 | pypy: {env:TOXPYTHON:pypy} 14 | py34: {env:TOXPYTHON:python3.4} 15 | py35: {env:TOXPYTHON:python3.5} 16 | {py36,docs,spell}: {env:TOXPYTHON:python3.6} 17 | py37: {env:TOXPYTHON:python3.7} 18 | {clean,check,report,extension-coveralls,coveralls,codecov}: python3.6 19 | setenv = 20 | PYTHONPATH={toxinidir}/tests 21 | PYTHONUNBUFFERED=yes 22 | passenv = 23 | * 24 | usedevelop = false 25 | deps = 26 | django 27 | pytest 28 | pytest-travis-fold 29 | pytest-cov 30 | pytest-django 31 | commands = 32 | {posargs:py.test --cov=django_github_webhook --cov-report=term-missing -vv tests} 33 | 34 | [testenv:spell] 35 | setenv = 36 | SPELLCHECK=1 37 | commands = 38 | sphinx-build -b spelling docs dist/docs 39 | skip_install = true 40 | deps = 41 | -r{toxinidir}/docs/requirements.txt 42 | sphinxcontrib-spelling 43 | pyenchant 44 | 45 | [testenv:docs] 46 | deps = 47 | -r{toxinidir}/docs/requirements.txt 48 | commands = 49 | sphinx-build {posargs:-E} -b html docs dist/docs 50 | sphinx-build -b linkcheck docs dist/docs 51 | 52 | [testenv:check] 53 | deps = 54 | docutils 55 | check-manifest 56 | flake8 57 | readme-renderer 58 | pygments 59 | skip_install = true 60 | commands = 61 | python setup.py check --strict --metadata --restructuredtext 62 | check-manifest {toxinidir} 63 | flake8 src tests setup.py 64 | 65 | [testenv:coveralls] 66 | deps = 67 | coveralls 68 | skip_install = true 69 | commands = 70 | coverage combine 71 | coverage report 72 | coveralls [] 73 | 74 | [testenv:codecov] 75 | deps = 76 | codecov 77 | skip_install = true 78 | commands = 79 | coverage combine 80 | coverage report 81 | coverage xml --ignore-errors 82 | codecov [] 83 | 84 | 85 | [testenv:report] 86 | deps = coverage 87 | skip_install = true 88 | commands = 89 | coverage combine 90 | coverage report 91 | coverage html 92 | 93 | [testenv:clean] 94 | commands = coverage erase 95 | skip_install = true 96 | deps = coverage 97 | 98 | --------------------------------------------------------------------------------