Skip to content
This repository was archived by the owner on Jun 25, 2021. It is now read-only.

Commit ecf15a7

Browse files
committed
Initial commit
0 parents  commit ecf15a7

File tree

12 files changed

+610
-0
lines changed

12 files changed

+610
-0
lines changed

.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Distribution / packaging
2+
.Python
3+
env/
4+
build/
5+
develop-eggs/
6+
dist/
7+
downloads/
8+
eggs/
9+
.eggs/
10+
lib/
11+
lib64/
12+
parts/
13+
sdist/
14+
var/
15+
*.egg-info/
16+
.installed.cfg
17+
*.egg
18+
.requirements/
19+
__pycache__/
20+
21+
# Serverless directories
22+
.serverless
23+
24+
# Node files
25+
node_modules/
26+
27+
# Project specific
28+
# Don't commit config.yaml
29+
config.yaml

LICENCE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2017 James Loh
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Serverless Webhooks
2+
3+
[![license](https://img.shields.io/github/license/jloh/dotfiles.svg)]() [![serverless](https://img.shields.io/badge/language-python-brightgreen.svg)]() [![serverless](https://img.shields.io/badge/serverless-1.22.0+-green.svg)]()
4+
5+
Serverless Webhooks is a small python project that digests webhooks from services that don't easily support push notifications and turns them into Pushbullet pushes. Depending on the number of events this project should fit under the free tier on AWS.
6+
7+
At this stage it supports GitHub, Discourse and Mailerlite webhooks inbound and only Pushbullet for notifications. Eventually I would like to move the Pushbullet code out to be more portable and support other platforms.
8+
9+
## Setup
10+
11+
### Enncrypted Variables
12+
13+
This project uses [AWS SSM](https://serverless.com/framework/docs/providers/aws/guide/variables/#reference-variables-using-the-ssm-parameter-store) environment variables to store secret tokens. A below example shows how to store them using the AWS CLI tools:
14+
15+
```
16+
aws ssm put-parameter --name /hooks/pushbullet_api_key --value <secure token in here> --type SecureString
17+
```
18+
19+
### Requirments
20+
21+
Install `serverless` and `serverless-python-requirements`:
22+
23+
```
24+
npm install -g serverless
25+
sls plugin install serverless-python-requirements
26+
```
27+
28+
Install python requirements
29+
**Note:** Its highly suggested you install the python requirments inside a virtualenv!
30+
31+
```
32+
pip install -r requirements.txt
33+
```
34+
35+
Now login to [Pushbullet](https://www.pushbullet.com/#settings) and generate an access token. This access token should be store as a [SSM variable](#requirments) under `/hooks/pushbullet_api_key`.
36+
37+
Now deploy your Serverless gateway:
38+
39+
```
40+
sls deploy
41+
```
42+
43+
### Endpoints
44+
45+
#### GitHub
46+
47+
1. Generate a [secret token](https://developer.github.com/webhooks/securing/)
48+
You could use `ruby -rsecurerandom -e 'puts SecureRandom.hex(20)'`
49+
1. Add your secret to [AWS SSM](#enncrypted-variables) under `/hooks/github_secret` eg:
50+
```
51+
aws ssm put-parameter --name /hooks/github_secret --value a3f7b3d530ab15e2f07df0324f8255cfcade49cd --type SecureString
52+
```
53+
1. Confirm your gateway that serverless created above (check it via `sls info`)
54+
1. Go to your repository settings -> [Webhooks](https://developer.github.com/webhooks/).
55+
1. Add a new webhook:
56+
* Payload URL -> Your serverless POST endpoint for GitHub
57+
* Content Type -> `application/json`
58+
* Secret -> The secret you generated above
59+
* Events -> The events configured in `config.yaml`
60+
1. Click add *Add Webhook*!
61+
62+
If you've done everything above correctly you should recieve a Ping event from GitHub via Pushbullet.
63+
64+
#### Discourse
65+
66+
1. Generate a secret to sign the payloads with
67+
You could use `ruby -rsecurerandom -e 'puts SecureRandom.hex(20)'`
68+
1. Add your secret to [AWS SSM](#enncrypted-variables) under `/hooks/discourse_secret` eg:
69+
```
70+
aws ssm put-parameter --name /hooks/discourse_secret --value a3f7b3d530ab15e2f07df0324f8255cfcade49cd --type SecureString
71+
```
72+
1. Confirm your gateway that serverless created above (check it via `sls info`)
73+
1. Go to the [Discourse Admin interface](https://meta.discourse.org/t/49045) -> API -> Webhooks -> New Webhook
74+
1. Enter your settings into Discourse:
75+
* Payload URL for the Discourse endpoint (`/v1/discourse`)
76+
* Content-Type -> `application/json`
77+
* Secret -> the one you generated above
78+
79+
If you've done everything above correctly try and send a ping event to your Discourse endpoint!
80+
81+
#### Mailerlite
82+
83+
Doco coming!
84+
85+
---
86+
87+
**Licence:** MIT

example.config.yaml

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
---
2+
github:
3+
events:
4+
ping:
5+
pushbullet:
6+
title: "[GitHub] {repository[full_name]}: ping"
7+
body: "Ping event for repo {repository[full_name]}"
8+
link: "{repository[html_url]}"
9+
always: true
10+
commit_comment:
11+
pushbullet:
12+
title: "[GitHub] {repository[full_name]}: {sender[login]} commented on {comment[commit_id]!s:.8}"
13+
body: "{body}"
14+
link: "{comment[html_url]}"
15+
create:
16+
pushbullet:
17+
title: "[GitHub] {repository[full_name]}: new {ref_type} created by {sender[login]}"
18+
body: "{ref_type}: {ref}"
19+
link: "{repository[html_url]}"
20+
delete:
21+
pushbullet:
22+
title: "[GitHub] {repository[full_name]}: {ref_type} deleted by {sender[login]}"
23+
body: "{ref_type}: {ref}"
24+
link: "{repository[html_url]}"
25+
fork:
26+
pushbullet:
27+
title: "[GitHub] {repository[full_name]}: {sender[login]} just forked your repo!"
28+
body: "Forked repo at {forkee[full_name]}"
29+
link: "{forkee[html_url]}"
30+
issues:
31+
pushbullet:
32+
title: "[GitHub] {repository[full_name]}: {sender[login]} {action} issue #{issue[number]!s}"
33+
body: "{issue[body]}"
34+
link: "{issue[html_url]}"
35+
issue_comment:
36+
pushbullet:
37+
title: "[GitHub] {repository[full_name]}: {sender[login]} {action} a comment on issue #{issue[number]!s}"
38+
body: "{comment[body]}"
39+
link: "{comment[html_url]}"
40+
member:
41+
pushbullet:
42+
title: "[GitHub] {repository[full_name]}: {member[login]} has been {action} "
43+
body: "Member was {action} by {sender[login]}"
44+
link: "{repository[html_url]}/settings/collaboration"
45+
always: true
46+
page_build:
47+
pushbullet:
48+
title: "[GitHub] {repository[full_name]}: {sender[login]} {build[status]} a new pages version"
49+
body: "{build[error][message]}"
50+
link: "{build[url]}"
51+
always: true
52+
pull_request:
53+
pushbullet:
54+
title: "[GitHub] {repository[full_name]}: {sender[login]} {action} PR #{number!s}"
55+
body: "{pull_request[body]}"
56+
link: "{pull_request[html_url]}"
57+
pull_request_review:
58+
pushbullet:
59+
title: "[GitHub] {repository[full_name]}: {sender[login]} {action} a review on PR #{pull_request[number]!s}"
60+
body: "{review[body]}"
61+
link: "{review[html_url]}"
62+
pull_request_review_comment:
63+
pushbullet:
64+
title: "[GitHub] {repository[full_name]}: {sender[login]} {action} a comment on PR #{pull_request[number]!s}"
65+
body: "{comment[body]}"
66+
link: "{comment[html_url]}"
67+
push:
68+
pushbullet:
69+
title: "[GitHub] {repository[full_name]}: new commit"
70+
body: "New commit ({head_commit[id]!s:.8}) by {pusher[name]}"
71+
link: "{compare}"
72+
watch:
73+
pushbullet:
74+
title: "[GitHub] {repository[full_name]}: {sender[login]} just stared your repository!"
75+
body: "You now have {repository[stargazers_count]!s} stars"
76+
link: "{sender[html_url]}"
77+
# List of users to ignore
78+
ignore:
79+
- 'dummy-user'
80+
81+
discourse:
82+
events:
83+
ping:
84+
pushbullet:
85+
title: "[Discourse] ping"
86+
body: "pong!"
87+
link: "https://meta.discourse.org"
88+
post_created:
89+
pushbullet:
90+
title: "[Discourse] New post by {post[username]}"
91+
body: "Post topic: {post[topic_slug]}"
92+
link: "https://meta.discourse.org/t/{post[topic_slug]}/{post[topic_id]}/{post[post_number]}/"
93+
topic_created:
94+
pushbullet:
95+
title: "[Discourse] New topic created: {topic[title]}"
96+
body: "Topic created by {topic[details][created_by][username]}"
97+
link: "https://meta.discourse.org/t/{topic[slug]}/{topic[id]}/"
98+
user_created:
99+
pushbullet:
100+
title: "[Discourse] New user created: {user[username]}"
101+
body: "Name: {user[name]}\n"
102+
link: "https://meta.discourse.org/u/{user[username]}"
103+
104+
updown_events:
105+
check.down:
106+
title: "[updown] {check[alias]} ({check[url]}) is failing"
107+
body: "Since: {downtime[started_at]}\nError: {check[error]}\nLast check: {check[last_check_at]}\nNext check: {check[next_check_at]}"
108+
check.up:
109+
title: "[updown] {check[alias]} ({check[url]}) is passing"
110+
body: "Since: {downtime[ended_at]}\nDuration: {downtime[duration]}\nLast status: {check[last_status]}"

requirements.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
certifi==2017.4.17
2+
chardet==3.0.4
3+
idna==2.5
4+
pushbullet.py==0.11.0
5+
python-magic==0.4.13
6+
PyYAML==3.12
7+
requests==2.18.1
8+
six==1.10.0
9+
urllib3==1.21.1
10+
websocket-client==0.44.0

serverless.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
service: serverless-webhooks
2+
3+
provider:
4+
name: aws
5+
runtime: python3.6
6+
# Global environment vars
7+
environment:
8+
pushbullet_api_key: '${ssm:/hooks/pushbullet_api_key~true}'
9+
10+
plugins:
11+
- serverless-python-requirements
12+
13+
package:
14+
exclude:
15+
# Exclude python env
16+
- env/**
17+
18+
functions:
19+
discourse:
20+
handler: webhooks/discourse.handler
21+
events:
22+
- http:
23+
path: v1/discourse
24+
method: post
25+
environment:
26+
discourse_secret: '${ssm:/hooks/discourse_secret~true}'
27+
github:
28+
handler: webhooks/github.handler
29+
events:
30+
- http:
31+
path: v1/github
32+
method: post
33+
environment:
34+
github_secret: '${ssm:/hooks/github_secret~true}'
35+
mailerlite:
36+
handler: webhooks/mailerlite.handler
37+
events:
38+
- http:
39+
path: v1/mailerlite
40+
method: post
41+
environment:
42+
mailerlite_secret: '${ssm:/hooks/mailerlite_secret~true}'

util/__init__.py

Whitespace-only changes.

util/funcs.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import hmac
2+
import hashlib
3+
from hashlib import sha1, sha256
4+
from sys import hexversion
5+
6+
# Verifys Signatures against digest/messages
7+
def verify_signature(secret, message, signature, digest):
8+
9+
# TODO: redo this shitty code
10+
if digest == 'sha1':
11+
digest = sha1
12+
elif digest == 'sha256':
13+
digest = sha256
14+
15+
# Turn out AWS body into bytes
16+
message_string = str(message)
17+
message_bytes = message_string.encode()
18+
19+
# Our secret is already a string but we need to turn it into bytes
20+
secret_bytes = secret.encode()
21+
22+
# Create MAC
23+
mac = hmac.new(secret_bytes, msg=message_bytes, digestmod=digest)
24+
25+
# Compare our signature against our message
26+
if hmac.compare_digest(str(mac.hexdigest()), str(signature)):
27+
return True
28+
else:
29+
return False

util/generate_config.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python3
2+
3+
# Gets our config from yaml and returns it!
4+
5+
import argparse, yaml
6+
7+
def parse_args():
8+
parser = argparse.ArgumentParser(description='Python Serverless Webhook server.')
9+
parser.add_argument('-c', '--config', type=argparse.FileType('r'), default='config.yaml',
10+
help='config file to load settings from')
11+
args, unknown = parser.parse_known_args()
12+
13+
return args
14+
15+
def load_config(config_file):
16+
print('Loading config file {}'.format(config_file.name))
17+
try:
18+
config = yaml.load(config_file)
19+
except yaml.YAMLError as e:
20+
exc_type, exc_obj, exc_tb = sys.exc_info()
21+
print('Error loading YAML {} on line {}'.format(e, exc_tb.tb_lineno))
22+
23+
return config
24+
25+
def return_config():
26+
"""
27+
Gets our config from YAML and returns it
28+
"""
29+
30+
args = parse_args()
31+
config = load_config(args.config)
32+
33+
return(config)
34+
35+
if __name__ == "__main__":
36+
return_config()

0 commit comments

Comments
 (0)