#!/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 ' 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 ' run_hook(_feedrepo, _fromhdr, _domain)