Skip to content

Commit 0e313f7

Browse files
committed
Update eventbridge-webhooks/2-github
1 parent 559d465 commit 0e313f7

3 files changed

Lines changed: 172 additions & 4 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""Webhook implementation for Github"""
2+
3+
import os
4+
import json
5+
import urllib.parse
6+
import base64
7+
import hmac
8+
import hashlib
9+
from cgi import parse_header
10+
import boto3
11+
import botocore
12+
import botocore.session
13+
from aws_secretsmanager_caching import SecretCache, SecretCacheConfig
14+
15+
16+
client = botocore.session.get_session().create_client('secretsmanager')
17+
cache_config = SecretCacheConfig()
18+
cache = SecretCache(config=cache_config, client=client)
19+
20+
github_webhook_secret_arn = os.environ.get('GITHUB_WEBHOOK_SECRET_ARN')
21+
event_bus_name = os.environ.get('EVENT_BUS_NAME', 'default')
22+
23+
event_bridge_client = boto3.client('events')
24+
25+
def _add_header(request, **kwargs):
26+
userAgentHeader = request.headers['User-Agent'] + ' fURLWebhook/1.0 (Github)'
27+
del request.headers['User-Agent']
28+
request.headers['User-Agent'] = userAgentHeader
29+
30+
event_system = event_bridge_client.meta.events
31+
event_system.register_first('before-sign.events.PutEvents', _add_header)
32+
33+
class PutEventError(Exception):
34+
"""Raised when Put Events Failed"""
35+
pass
36+
37+
def lambda_handler(event, _context):
38+
"""Webhook function"""
39+
headers = event.get('headers')
40+
41+
# Input validation
42+
try:
43+
json_payload = get_json_payload(event=event)
44+
except ValueError as err:
45+
print_error(f'400 Bad Request - {err}', headers)
46+
return {'statusCode': 400, 'body': str(err)}
47+
except BaseException as err: # Unexpected Error
48+
print_error('500 Internal Server Error\n' +
49+
f'Unexpected error: {err}, {type(err)}', headers)
50+
return {'statusCode': 500, 'body': 'Internal Server Error'}
51+
52+
detail_type = headers.get('x-github-event', 'github-webhook-lambda')
53+
try:
54+
if not contains_valid_signature(event=event):
55+
print_error('401 Unauthorized - Invalid Signature', headers)
56+
return {'statusCode': 401, 'body': 'Invalid Signature'}
57+
58+
response = forward_event(json_payload, detail_type)
59+
60+
if response['FailedEntryCount'] > 0:
61+
print_error('500 FailedEntry Error - The event was not successfully forwarded to Amazon EventBridge\n' +
62+
str(response['Entries'][0]), headers)
63+
return {'statusCode': 500, 'body': 'FailedEntry Error - The entry could not be succesfully forwarded to Amazon EventBridge'}
64+
65+
return {'statusCode': 202, 'body': 'Message forwarded to Amazon EventBridge'}
66+
67+
except PutEventError as err:
68+
print_error(f'500 Put Events Error - {err}', headers)
69+
return {'statusCode': 500, 'body': 'Internal Server Error - The request was rejected by Amazon EventBridge API'}
70+
71+
except BaseException as err: # Unexpected Error
72+
print_error('500 Internal Server Error\n' +
73+
f'Unexpected error: {err}, {type(err)}', headers)
74+
return {'statusCode': 500, 'body': 'Internal Server Error'}
75+
76+
77+
def normalize_payload(raw_payload, is_base64_encoded):
78+
"""Decode payload if needed"""
79+
if raw_payload is None:
80+
raise ValueError('Missing event body')
81+
if is_base64_encoded:
82+
return base64.b64decode(raw_payload).decode('utf-8')
83+
return raw_payload
84+
85+
86+
def contains_valid_signature(event):
87+
"""Check for the payload signature
88+
Github documention: https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks#validating-payloads-from-github
89+
"""
90+
secret = cache.get_secret_string(github_webhook_secret_arn)
91+
payload_bytes = get_payload_bytes(
92+
raw_payload=event['body'], is_base64_encoded=event['isBase64Encoded'])
93+
computed_signature = compute_signature(
94+
payload_bytes=payload_bytes, secret=secret)
95+
96+
return hmac.compare_digest(event['headers'].get('x-hub-signature-256', ''), computed_signature)
97+
98+
99+
def get_payload_bytes(raw_payload, is_base64_encoded):
100+
"""Get payload bytes to feed hash function"""
101+
if is_base64_encoded:
102+
return base64.b64decode(raw_payload)
103+
else:
104+
return raw_payload.encode()
105+
106+
107+
def compute_signature(payload_bytes, secret):
108+
"""Compute HMAC-SHA256"""
109+
m = hmac.new(key=secret.encode(), msg=payload_bytes,
110+
digestmod=hashlib.sha256)
111+
return 'sha256=' + m.hexdigest()
112+
113+
114+
def get_json_payload(event):
115+
"""Get JSON string from payload"""
116+
content_type = get_content_type(event.get('headers', {}))
117+
if not (content_type == 'application/json' or
118+
content_type == 'application/x-www-form-urlencoded'):
119+
raise ValueError('Unsupported content-type')
120+
121+
payload = normalize_payload(
122+
raw_payload=event.get('body'),
123+
is_base64_encoded=event['isBase64Encoded'])
124+
125+
if content_type == 'application/x-www-form-urlencoded':
126+
parsed_qs = urllib.parse.parse_qs(payload)
127+
if 'payload' not in parsed_qs or len(parsed_qs['payload']) != 1:
128+
raise ValueError('Invalid urlencoded payload')
129+
130+
payload = parsed_qs['payload'][0]
131+
132+
try:
133+
json.loads(payload)
134+
135+
except ValueError as err:
136+
raise ValueError('Invalid JSON payload') from err
137+
138+
return payload
139+
140+
141+
def forward_event(payload, detail_type):
142+
"""Forward event to EventBridge"""
143+
try :
144+
return event_bridge_client.put_events(
145+
Entries=[
146+
{
147+
'Source': 'github.com',
148+
'DetailType': detail_type,
149+
'Detail': payload,
150+
'EventBusName': event_bus_name
151+
},
152+
]
153+
)
154+
except BaseException as err:
155+
raise PutEventError('Put Events Failed')
156+
157+
def get_content_type(headers):
158+
"""Helper function to parse content-type from the header"""
159+
raw_content_type = headers.get('content-type')
160+
161+
if raw_content_type is None:
162+
return None
163+
content_type, _ = parse_header(raw_content_type)
164+
return content_type
165+
166+
167+
def print_error(message, headers):
168+
"""Helper function to print errors"""
169+
print(f'ERROR: {message}\nHeaders: {str(headers)}')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
boto3

eventbridge-webhooks/2-github/template.yaml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,9 @@ Resources:
5454
"InboundWebhook-Lambda-${ID}",
5555
ID: !Select [2, !Split ["/", !Ref AWS::StackId]],
5656
] # Append the stack UUID
57-
CodeUri:
58-
Bucket: !Sub 'eventbridge-inbound-webhook-templates-prod-${AWS::Region}'
59-
Key: 'lambda-templates/github-lambdasrc.zip'
57+
CodeUri: ./src
6058
Handler: app.lambda_handler
61-
Runtime: python3.8
59+
Runtime: python3.13
6260
ReservedConcurrentExecutions: 10
6361
Environment:
6462
Variables:

0 commit comments

Comments
 (0)