blob: ecc573f06cc958ba61750e7209cd309695edbd1c [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# This is the post-receive git hook used to generate the activity feed
# public-inbox repository. It requires that ezpi is installed
# https://sr.ht/~monsieuricon/ezpi/
#
# Copyright (C) 2020 by The Linux Foundation
# SPDX-License-Identifier: GPL-2.0-or-later
#
__author__ = 'Konstantin Ryabitsev <konstantin@linuxfoundation.org>'
import os
import sys
import ezpi # noqa
import hashlib
import base64
from email.message import EmailMessage
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import Optional
def get_config_from_git(regexp: str, defaults: Optional[dict] = None) -> dict:
gitconfig = defaults if defaults else dict()
args = ['config', '-z', '--get-regexp', regexp]
ee, out, err = ezpi.git_run_command('', args)
if ee > 0 or not len(out):
return gitconfig
for line in out.decode().split('\x00'):
if not line:
continue
key, value = line.split('\n', 1)
try:
chunks = key.split('.')
cfgkey = chunks[-1]
gitconfig[cfgkey.lower()] = value
except ValueError:
pass
return gitconfig
def run_hook(feedrepo: str, fromhdr: str, domain: str):
# Look if we have a GL_USER and GL_REPO in the env
user = os.getenv('GL_USER')
if not user:
user = os.getenv('USER')
repo = os.getenv('GL_REPO')
if not repo:
repo = os.getcwd()
ll = list()
attachments = dict()
ll.append('---')
ll.append('service: git-receive-pack')
ll.append(f'repo: {repo}')
ll.append(f'user: {user}')
# Do we have a ~/.activity-feed-secret?
secret = None
secretf = os.path.expanduser('~/.activity-feed-secret')
# The idea is to rotate it frequently, with the value logged in syslog.
# This allows us to see if a push is coming from the same remote IP address,
# but only within the same calendar day.
try:
with open(secretf) as fh:
secret = fh.read().strip()
except (FileNotFoundError, IOError):
pass
if secret:
conn_info = os.getenv('SSH_CONNECTION')
if conn_info:
remote_ip = conn_info.split()[0]
ipline = f'{secret}{user}{remote_ip}'
iph = hashlib.sha1()
iph.update(ipline.encode())
hashed = base64.b64encode(iph.digest()).decode()
ll.append(f'remote_ip: {hashed}')
# Do we have a push cert?
cert = os.getenv('GIT_PUSH_CERT')
if cert:
gpcstatus = os.getenv('GIT_PUSH_CERT_STATUS')
ll.append(f'git_push_cert_status: {gpcstatus}')
args = ['cat-file', 'blob', cert]
ee, out, err = ezpi.git_run_command('', args)
if ee == 0 and out:
attachments['git-push-certificate.txt'] = out.decode()
ll.append('changes:')
seenranges = dict()
while True:
line = sys.stdin.readline()
if not line:
break
oldrev, newrev, ref = line.strip().split()
ll.append(f' - ref: {ref}')
ll.append(f' old: {oldrev}')
ll.append(f' new: {newrev}')
if (oldrev, newrev) not in seenranges:
args = ['rev-list', '--max-count=1024', '--reverse', '--pretty=oneline', newrev]
if set(oldrev) != {0}:
args += [f'^{oldrev}']
ee, out, err = ezpi.git_run_command('', args)
if ee > 0 or not len(out):
continue
seenranges[(oldrev, newrev)] = out
else:
out = seenranges[(oldrev, newrev)]
if len(out) > 1024:
# Add it as attachment, unless we already have one with this name
filename = f'revlist-{oldrev[:12]}-{newrev[:12]}.txt'
if filename not in attachments:
attachments[filename] = out.decode()
ll.append(f' log: {filename}')
continue
ll.append(' log: |')
for pretty in out.decode().split('\n'):
ll.append(f' {pretty}')
body = '\n'.join(ll) + '\n'
if attachments:
msg = MIMEMultipart()
msg.attach(MIMEText(body, 'plain'))
for attfilename, attbody in attachments.items():
att = MIMEText(attbody, 'plain')
att.add_header('Content-Disposition', f'attachment; filename={attfilename}')
msg.attach(att)
else:
msg = EmailMessage()
msg.set_payload(body)
msg['From'] = fromhdr
msg['Subject'] = f'post-receive: {repo}'
try:
ezpi.add_rfc822(feedrepo, msg, domain)
sys.stderr.write('Recorded in the transparency log\n')
ezpi.run_hook(feedrepo)
except RuntimeError:
# Could not add it to the feed, complain
sys.stderr.write('FAILED writing to the transparency log!\n')
if __name__ == '__main__':
if sys.stdin.isatty():
# Nothing passed via stdin, so nothing to add to the feed
sys.exit(0)
config = get_config_from_git(r'activityfeed\..*')
_feedrepo = config.get('repo')
if not config.get('repo'):
# The audit repo is not defined in gitconfig, so nothing for us to do.
sys.exit(0)
_fromhdr = config.get('from')
_domain = config.get('domain')
if not _domain:
_domain = 'localhost'
if not _fromhdr:
_fromhdr = f'Post-Receive Hook <post-receive@{_domain}>'
run_hook(_feedrepo, _fromhdr, _domain)