Blob Blame History Raw
#!/usr/bin/python
#
# CGI script to handle file updates for the rpms CVS repository. There
# is nothing really complex here other than tedious checking of our
# every step along the way...
#
# Written for Fedora, modified to suit CentOS Infrastructure.
# Modified by Howard Johnson <merlin@merlinthp.org> 2014
#
# License: GPL

#
# centos' lookaside is a bit differently laid out to fedora's.
# centos uses a <package>/<branch>/<sha1sum> scheme.
#
# The upload.cgi gets called with the following arguments:
#   name - package (git repo) name
#   branch - branch name
#   sha1sum - SHA1 checksum of the file
#   file - the file to upload (optional)
#
# With only the first three args, the script runs in check mode.
# With the fourth too, it operates in upload mode.
#

import os
import sys
import cgi
import tempfile
import syslog
import smtplib
import re
from ConfigParser import SafeConfigParser

from email import Header, Utils
try:
    from email.mime.text import MIMEText
except ImportError:
    from email.MIMEText import MIMEText

import hashlib
sha1_constructor = hashlib.sha1

# Config file with all our settings
CONFIG = '/etc/lookaside.cfg'
conf = SafeConfigParser()
conf.read(CONFIG)

# Reading buffer size
BUFFER_SIZE = 4096

# Gitblit config file regexes
SECTION = re.compile(r'\[(.+)\]')
OPTION = re.compile(r'(.+)=(.+)')
REPO = re.compile(r'([^/]+).git$')

def merge_gitblit_section(repoacl, repos, users):
    for repo in repos:
        if repo not in repoacl:
            repoacl[repo] = []
        for user in users:
            if user not in repoacl[repo]:
                repoacl[repo].append(user)

# turns the gitblit config file into a dict of package name to permitted users
def parse_gitblit_config(filename):
    insection = False
    repoacl = {}
    sectrepos = []
    sectusers = []

    f = open(filename)
    for line in f:
        if line.strip() == '' or line[0] == '#':
            continue
        secth = SECTION.match(line)
        if secth:
            if insection:
                merge_gitblit_section(repoacl, sectrepos, sectusers)

                sectrepos = []
                sectusers = []
            else:
                insection = True
            continue
        opt = OPTION.match(line)
        if opt:
            if not insection:
                continue
            key = opt.group(1).strip()
            value = opt.group(2).strip()

            if key == "repository":
                pack = REPO.search(value)
                if pack:
                    sectrepos.append(pack.group(1))
            elif key == "user":
                sectusers.append(value)
    if insection:
        merge_gitblit_section(repoacl, sectrepos, sectusers)
    f.close()
    return repoacl

def send_error(text):
    print text
    sys.exit(1)

def check_form(form, var):
    ret = form.getvalue(var, None)
    if ret is None:
        send_error('Required field "%s" is not present.' % var)
    if isinstance(ret, list):
        send_error('Multiple values given for "%s". Aborting.' % var)
    return ret

def send_email(pkg, sha1, filename, username):
    text = """A file has been added to the lookaside cache for %(pkg)s:

%(sha1)s  %(filename)s""" % locals()
    msg = MIMEText(text)
    sender_name = conf.get('mail', 'sender_name')
    sender_email = conf.get('mail', 'sender_email')
    sender = Utils.formataddr((sender_name, sender_email))
    recipient = conf.get('mail', 'recipient')
    msg['Subject'] = 'File %s uploaded to lookaside cache by %s' % (
            filename, username)
    msg['From'] = sender
    msg['To'] = recipient
    try:
        s = smtplib.SMTP(conf.get('mail', 'smtp_server'))
        s.sendmail(sender, recipient, msg.as_string())
    except:
        errstr = 'sending mail for upload of %s failed!' % filename
        print >> sys.stderr, errstr
        syslog.syslog(errstr)

def main():
    os.umask(002)

    username = os.environ.get('SSL_CLIENT_S_DN_CN', None)

    print 'Content-Type: text/plain'
    print

    assert os.environ['REQUEST_URI'].split('/')[1] == 'lookaside'

    form = cgi.FieldStorage()
    name = check_form(form, 'name')
    branch = check_form(form, 'branch')
    sha1sum = check_form(form, 'sha1sum')

    action = None
    upload_file = None
    filename = None

    # Is this a submission or a test?
    # in a test, we don't get a file.
    if not form.has_key('file'):
        action = 'check'
        print >> sys.stderr, '[username=%s] Checking file status: NAME=%s BRANCH=%s SHA1SUM=%s' % (username, name, branch, sha1sum)
    else:
        action = 'upload'
        upload_file = form['file']
        if not upload_file.file:
            send_error('No file given for upload. Aborting.')
        filename = os.path.basename(upload_file.filename)
        print >> sys.stderr, '[username=%s] Processing upload request: NAME=%s BRANCH=%s SHA1SUM=%s' % (username, name, branch, sha1sum)

    module_dir = os.path.join(conf.get('lookaside', 'cache_dir'), name, branch)
    dest_file =  os.path.join(module_dir, sha1sum)

    # try to see if we already have this file...
    if os.path.exists(dest_file):
        if action == 'check':
            print 'Available'
        else:
            upload_file.file.close()
            dest_file_stat = os.stat(dest_file)
            print 'File %s already exists' % filename
            print 'File: %s Size: %d' % (dest_file, dest_file_stat.st_size)
        sys.exit(0)
    elif action == 'check':
        print 'Missing'
        sys.exit(0)

    # if desired, make sure the user has permission to write to this branch
    if conf.getboolean('acl', 'do_acl'):
        # load in the gitblit config
        repoacl = parse_gitblit_config(conf.get('acl', 'gitblit_config'))

        # if the package isn't in the gitblit config, we can't give upload perms
        if name not in repoacl:
            send_error("Unknown package %s" % name)

        # now check the perms
        if username not in repoacl[name]:
            send_error("Write access package %s rejected for user %s" % (name, username))

    # check that all directories are in place
    if not os.path.isdir(module_dir):
        os.makedirs(module_dir, 02775)

    # grab a temporary filename and dump our file in there
    tempfile.tempdir = module_dir
    tmpfile = tempfile.mkstemp(sha1sum)[1]
    tmpfd = open(tmpfile, 'w')

    # now read the whole file in
    m = sha1_constructor()
    filesize = 0
    while True:
        data = upload_file.file.read(BUFFER_SIZE)
        if not data:
            break
        tmpfd.write(data)
        m.update(data)
        filesize += len(data)

    # now we're done reading, check the MD5 sum of what we got
    tmpfd.close()
    check_sha1sum = m.hexdigest()
    if sha1sum != check_sha1sum:
        os.unlink(tmpfile)
        send_error("SHA1 check failed. Received %s instead of %s." % (check_sha1sum, sha1sum))

    # rename it its final name
    os.rename(tmpfile, dest_file)
    os.chmod(dest_file, 0644)

    print >> sys.stderr, '[username=%s] Stored %s (%d bytes)' % (username, dest_file, filesize)
    print 'File %s size %d SHA1 %s stored OK' % (filename, filesize, sha1sum)
    if conf.getboolean('mail', 'send_mail'):
        send_email(name, sha1sum, filename, username)

if __name__ == '__main__':
    main()