from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
import json
import logging
http_server_logger = logging.getLogger(__name__)
[docs]class MyHTTPServer(HTTPServer):
def __init__(self, token, hooks, bots, *args, **kw):
HTTPServer.__init__(self, *args, **kw)
self.token = token
self.hooks = hooks
self.bots = bots
[docs]class RequestException(Exception):
def __init__(self, code, status):
self.code = code
self.status = status
http_server_logger.error('%s - %s' % (self.code, self.status))
[docs]class RequestHandler(BaseHTTPRequestHandler):
"""A POST request handler."""
def _check_token(self):
# get gitlab secret token
gitlab_token_header = self.headers.get('X-Gitlab-Token')
if not gitlab_token_header:
raise RequestException(400, "'X-Gitlab-Token' header not found")
# get token from config file
gitlab_token = str(self.server.token)
# Check if the gitlab token is valid
if gitlab_token_header != gitlab_token:
raise RequestException(401, "Gitlab token not authorized")
def _check_and_get_request_data(self):
# get payload
header_length = int(self.headers.get('content-length', "0"))
json_payload = self.rfile.read(header_length)
if len(json_payload) == 0:
raise RequestException(400, "Request didn't contain data")
try:
try:
json_params = json.loads(json_payload)
except TypeError:
# Python 3.5 json.loads doesn't support binary input
# https://docs.python.org/3/whatsnew/3.6.html#json
json_params = json.loads(json_payload.decode('utf-8'))
except json.decoder.JSONDecodeError:
raise RequestException(400, "JSON data couldn't be parsed")
object_kind = json_params.get('object_kind')
if not object_kind:
raise RequestException(400, "Missing 'object_kind'")
if object_kind not in ['push', 'issue', 'merge_request']:
raise RequestException(400, "object_kind '%s' not supported" %
object_kind)
return json_params
[docs] def do_POST(self):
http_server_logger.info("Hook received")
json_params = None
try:
self._check_token()
json_params = self._check_and_get_request_data()
handler = getattr(
self, '_handle_%s' % json_params.get('object_kind'))
handler(json_params)
except RequestException as re:
if json_params:
http_server_logger.debug('JSON PARAMS: %s' % json_params)
http_server_logger.exception(re.status)
self.send_response(re.code, re.status)
except Exception:
http_server_logger.exception("Internal server error")
self.send_response(500, "Internal server error")
finally:
self.end_headers()
def _handle_push(self, json_params):
http_server_logger.info('handling push')
try:
project = json_params['project']['path_with_namespace']
project_name = json_params['project']['name']
user = json_params['user_username']
commits = json_params['commits']
num_commits = json_params.get('total_commits_count')
branch_name = json_params['ref']
ref_after = json_params['after']
ref_prefix = 'refs/heads/'
if branch_name.startswith(ref_prefix):
branch_name = branch_name[len(ref_prefix):]
except KeyError:
raise RequestException(400, "Missing data in the request")
if not num_commits:
# Branch created or deleted
action = 'created'
if ref_after == '0000000000000000000000000000000000000000':
action = 'deleted'
msg = ('{user} {action} branch {branch_name} on {project_name}.'
.format(user=user, action=action,
branch_name=branch_name,
project_name=project_name))
else:
last_commit = commits[-1]
last_commit_msg = last_commit['message'].split('\n')[0].strip()
pre_msg = ('{user} pushed on {project_name}@{branch_name}:'
.format(user=user, project_name=project_name,
branch_name=branch_name))
if num_commits == 1:
msg = ('{pre_msg} {last_commit_msg}'
.format(pre_msg=pre_msg,
last_commit_msg=last_commit_msg))
else:
msg = ('{pre_msg} {num_commits} commits (last: '
'{last_commit_msg})'
.format(pre_msg=pre_msg,
num_commits=num_commits,
last_commit_msg=last_commit_msg))
self._send_message_to_all('push', project, msg, branch=branch_name)
def _handle_issue(self, json_params):
http_server_logger.info('handling issue')
try:
user = json_params['user']['username']
project_name = json_params['project']['name']
project = json_params['project']['path_with_namespace']
issue = json_params['object_attributes']
issue_number = issue['iid']
issue_title = issue['title'].strip()
issue_action = issue['action']
url = issue['url']
except KeyError:
raise RequestException(400, "Missing data in the request")
display_action = self.simple_past(issue_action)
hook_key = 'issue'
if issue_action == 'update':
changes = json_params['changes']
if "labels" in changes:
previous = [label['title']
for label in changes['labels']['previous']]
current = [label['title']
for label in changes['labels']['current']]
added = list(set(current).difference(previous))
removed = list(set(previous).difference(current))
# Be deterministic
added.sort()
removed.sort()
added_quoted = ", ".join(["'%s'" % l for l in added])
removed_quoted = ", ".join(["'%s'" % l for l in removed])
chg_msg = ""
if added:
chg_msg += "added %s " % added_quoted
if added and removed:
chg_msg += "and "
if removed:
chg_msg += "removed %s " % removed_quoted
chg_msg += "label(s) to"
hook_key = 'issue_label'
else:
# Unsupported update hook
self.send_response(200, "OK")
return
display_action = chg_msg
msg = ('{user} {display_action} issue #{issue_number} '
'({issue_title}) on {project_name} {url}'
.format(user=user, display_action=display_action,
issue_number=issue_number, issue_title=issue_title,
project_name=project_name, url=url))
self._send_message_to_all(
hook_key, project, msg, 'issue', issue_number)
def _handle_merge_request(self, json_params):
http_server_logger.info('handling merge_request')
try:
user = json_params['user']['username']
request = json_params['object_attributes']
project = request['target']['path_with_namespace']
project_name = request['target']['name']
from_branch = request['source_branch']
to_branch = request['target_branch']
request_number = request['iid']
request_title = request['title'].strip()
request_action = request['action']
request_wip = request['work_in_progress']
url = request['url']
except KeyError:
raise RequestException(400, "Missing data in the request")
display_action = self.simple_past(request_action)
hook_key = 'merge_request'
if request_action == 'update':
changes = json_params['changes']
chg_msg = ""
if "oldrev" in request:
hook_key = 'merge_request_update'
chg_msg = "updated commits on"
elif "title" in changes:
def is_title_wip(title):
title_lower = title.lower()
flags = ["[WIP]", "WIP:", "WIP "]
for flag in flags:
if flag.lower() in title_lower:
return True
return False
was_wip = is_title_wip(changes['title']['previous'])
if was_wip and not request_wip:
chg_msg = "opened (was WIP)"
else:
hook_key = 'merge_request_title'
chg_msg = "changed title of"
elif "assignee" in changes:
hook_key = 'merge_request_assignee'
previous = changes['assignee']['previous']
current = changes['assignee']['current']
# If current is empty, it's an unassignment
if current:
chg_msg = "assigned %s to" % current['username']
else:
chg_msg = "unassigned %s from" % previous['username']
else:
# Unsupported update hook
self.send_response(200, "OK")
return
display_action = chg_msg
msg = ('{user} {display_action} MR !{request_number} '
'({from_branch}->{to_branch}: {request_title}) '
'on {project_name} {url}'
.format(user=user, display_action=display_action,
request_number=request_number, from_branch=from_branch,
to_branch=to_branch, request_title=request_title,
project_name=project_name, url=url))
self._send_message_to_all(
hook_key, project, msg, 'merge_request', request_number,
is_wip=request_wip)
[docs] @staticmethod
def simple_past(verb):
if verb.endswith('ed'):
return verb
if not verb.endswith('e'):
verb = verb + 'e'
verb = verb + 'd'
return verb
def _send_message_to_all(self, hook_key, project, msg,
kind=None, number=None, branch=None,
is_wip=False):
hooks = self.server.hooks
bots = self.server.bots
for h in hooks:
if h['project'] == project:
network = h['network']
reports = h['reports']
branches = h.get('branches', '').split()
show_wips = h.get('wip', False)
bot = bots[network]['bot']
if branch and branch not in branches:
continue
if not show_wips and is_wip:
continue
for r in reports:
if hook_key in reports[r]:
http_server_logger.info('sending to %s, in network %s'
% (r, network))
bot.connection.privmsg(r, msg)
if kind and number:
bot._update_mentions(r, kind, number)
self.send_response(200, "OK")