2015-03-25 12:22:45 +01:00
|
|
|
#!/usr/bin/python
|
|
|
|
#
|
|
|
|
# CGI script to handle file updates for the rpms git repository. There
|
|
|
|
# is nothing really complex here other than tedious checking of our
|
|
|
|
# every step along the way...
|
|
|
|
#
|
|
|
|
# License: GPL
|
|
|
|
|
2016-10-05 12:43:12 +02:00
|
|
|
import cgi
|
|
|
|
import errno
|
|
|
|
import grp
|
|
|
|
import hashlib
|
2015-03-25 12:22:45 +01:00
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
import fedmsg
|
|
|
|
import fedmsg.config
|
|
|
|
|
2017-02-23 17:06:20 +01:00
|
|
|
from configparser import ConfigParser
|
2015-03-25 12:22:45 +01:00
|
|
|
|
|
|
|
# Reading buffer size
|
|
|
|
BUFFER_SIZE = 4096
|
|
|
|
|
|
|
|
# Fedora Packager Group
|
|
|
|
PACKAGER_GROUP = 'packager'
|
|
|
|
|
2015-03-25 14:09:05 +01:00
|
|
|
|
2016-10-05 12:43:12 +02:00
|
|
|
def send_error(text, status='500 Internal Server Error'):
|
|
|
|
"""Send an error back to the client
|
|
|
|
|
|
|
|
This ensures that the client will get a proper error, including the HTTP
|
|
|
|
status code, so that it can handle problems appropriately.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
text (str): The error message to send the client
|
|
|
|
status (str, optional): The HTTP status code to return to the client.
|
|
|
|
"""
|
|
|
|
print 'Status: %s' % status
|
|
|
|
print 'Content-type: text/plain'
|
|
|
|
print
|
2015-03-25 12:22:45 +01:00
|
|
|
print text
|
2016-10-05 12:43:12 +02:00
|
|
|
sys.exit(0)
|
|
|
|
|
2015-03-25 12:22:45 +01:00
|
|
|
|
|
|
|
def check_form(form, var):
|
|
|
|
ret = form.getvalue(var, None)
|
|
|
|
if ret is None:
|
2016-10-05 12:43:12 +02:00
|
|
|
send_error('Required field "%s" is not present.' % var,
|
|
|
|
status='400 Bad Request')
|
2015-03-25 12:22:45 +01:00
|
|
|
if isinstance(ret, list):
|
2016-10-05 12:43:12 +02:00
|
|
|
send_error('Multiple values given for "%s". Aborting.' % var,
|
|
|
|
status='400 Bad Request')
|
2015-03-25 12:22:45 +01:00
|
|
|
return ret
|
|
|
|
|
2016-10-05 12:43:12 +02:00
|
|
|
|
2015-03-25 12:22:45 +01:00
|
|
|
def check_auth(username):
|
|
|
|
authenticated = False
|
|
|
|
try:
|
|
|
|
if username in grp.getgrnam(PACKAGER_GROUP)[3]:
|
|
|
|
authenticated = True
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
return authenticated
|
|
|
|
|
2015-03-25 14:09:05 +01:00
|
|
|
|
2016-10-05 12:43:12 +02:00
|
|
|
def hardlink(src, dest, username):
|
|
|
|
makedirs(os.path.dirname(dest), username)
|
|
|
|
|
|
|
|
try:
|
|
|
|
os.link(src, dest)
|
|
|
|
|
|
|
|
except OSError as e:
|
|
|
|
if e.errno != errno.EEXIST:
|
|
|
|
send_error(str(e))
|
|
|
|
|
|
|
|
# The file already existed at the dest path, hardlink over it
|
|
|
|
os.unlink(dest)
|
|
|
|
os.link(src, dest)
|
|
|
|
|
|
|
|
sys.stderr.write("[username=%s] ln %s %s\n" % (username, src, dest))
|
|
|
|
|
2015-03-25 14:09:05 +01:00
|
|
|
|
2016-10-05 12:43:12 +02:00
|
|
|
def makedirs(dir_, username, mode=02755):
|
|
|
|
try:
|
|
|
|
os.makedirs(dir_, mode=mode)
|
|
|
|
sys.stderr.write('[username=%s] mkdir %s\n' % (username, dir_))
|
|
|
|
|
|
|
|
except OSError as e:
|
|
|
|
if e.errno != errno.EEXIST:
|
|
|
|
send_error(str(e))
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
2017-02-23 17:06:20 +01:00
|
|
|
config = ConfigParser()
|
|
|
|
config.read('/etc/dist-git/dist-git.conf')
|
|
|
|
|
2015-03-25 12:22:45 +01:00
|
|
|
os.umask(002)
|
|
|
|
|
|
|
|
username = os.environ.get('SSL_CLIENT_S_DN_CN', None)
|
2017-02-02 15:16:42 +01:00
|
|
|
gssname = os.environ.get('GSS_NAME', None)
|
|
|
|
if gssname and '@' in gssname and not username:
|
|
|
|
username = gssname.partition('@')[0]
|
2015-03-25 12:22:45 +01:00
|
|
|
if not check_auth(username):
|
2016-10-05 12:43:12 +02:00
|
|
|
send_error('You must connect with a valid certificate and be in the '
|
|
|
|
'%s group to upload.' % PACKAGER_GROUP,
|
|
|
|
status='403 Forbidden')
|
2015-03-25 12:22:45 +01:00
|
|
|
|
|
|
|
print 'Content-Type: text/plain'
|
|
|
|
print
|
|
|
|
|
|
|
|
assert os.environ['REQUEST_URI'].split('/')[1] == 'repo'
|
|
|
|
|
|
|
|
form = cgi.FieldStorage()
|
|
|
|
name = check_form(form, 'name')
|
|
|
|
|
|
|
|
# Search for the file hash, start with stronger hash functions
|
2016-10-05 12:43:12 +02:00
|
|
|
if 'sha512sum' in form:
|
2015-03-25 12:22:45 +01:00
|
|
|
checksum = check_form(form, 'sha512sum')
|
|
|
|
hash_type = "sha512"
|
|
|
|
|
2016-10-05 12:43:12 +02:00
|
|
|
elif 'md5sum' in form:
|
2015-03-25 12:22:45 +01:00
|
|
|
# Fallback on md5, as it's what we currently use
|
|
|
|
checksum = check_form(form, 'md5sum')
|
|
|
|
hash_type = "md5"
|
|
|
|
|
|
|
|
else:
|
2016-10-05 12:43:12 +02:00
|
|
|
send_error('Required checksum is not present.',
|
|
|
|
status='400 Bad Request')
|
2015-03-25 12:22:45 +01:00
|
|
|
|
|
|
|
action = None
|
|
|
|
upload_file = None
|
|
|
|
filename = None
|
|
|
|
|
|
|
|
# Is this a submission or a test?
|
|
|
|
# in a test, we don't get a file, just a filename.
|
|
|
|
# In a submission, we don;t get a filename, just the file.
|
2016-10-05 12:43:12 +02:00
|
|
|
if 'filename' in form:
|
2015-03-25 12:22:45 +01:00
|
|
|
action = 'check'
|
|
|
|
filename = check_form(form, 'filename')
|
|
|
|
filename = os.path.basename(filename)
|
2016-10-05 12:43:12 +02:00
|
|
|
sys.stderr.write('[username=%s] Checking file status: NAME=%s '
|
|
|
|
'FILENAME=%s %sSUM=%s\n' % (username, name, filename,
|
|
|
|
hash_type.upper(),
|
|
|
|
checksum))
|
2015-03-25 12:22:45 +01:00
|
|
|
else:
|
|
|
|
action = 'upload'
|
2016-10-05 12:43:12 +02:00
|
|
|
if 'file' in form:
|
2015-03-25 12:22:45 +01:00
|
|
|
upload_file = form['file']
|
|
|
|
if not upload_file.file:
|
2016-10-05 12:43:12 +02:00
|
|
|
send_error('No file given for upload. Aborting.',
|
|
|
|
status='400 Bad Request')
|
2015-03-25 12:22:45 +01:00
|
|
|
filename = os.path.basename(upload_file.filename)
|
|
|
|
else:
|
2016-10-05 12:43:12 +02:00
|
|
|
send_error('Required field "file" is not present.',
|
|
|
|
status='400 Bad Request')
|
|
|
|
|
|
|
|
sys.stderr.write('[username=%s] Processing upload request: '
|
|
|
|
'NAME=%s FILENAME=%s %sSUM=%s\n' % (
|
|
|
|
username, name, filename, hash_type.upper(),
|
|
|
|
checksum))
|
2015-03-25 12:22:45 +01:00
|
|
|
|
2017-02-23 17:06:20 +01:00
|
|
|
module_dir = os.path.join(config['dist-git']['cache_dir'], "lookaside/pkgs", name)
|
2015-03-25 12:22:45 +01:00
|
|
|
hash_dir = os.path.join(module_dir, filename, hash_type, checksum)
|
2016-10-05 12:43:12 +02:00
|
|
|
msgpath = os.path.join(name, filename, hash_type, checksum, filename)
|
2015-03-25 12:22:45 +01:00
|
|
|
|
|
|
|
# first test if the module really exists
|
2017-04-08 15:15:36 +02:00
|
|
|
git_dir = os.path.join(config['dist-git']['gitroot_dir'], '%s.git' % name)
|
2015-03-25 12:22:45 +01:00
|
|
|
if not os.path.isdir(git_dir):
|
2016-10-05 12:43:12 +02:00
|
|
|
sys.stderr.write('[username=%s] Unknown module: %s' % (username, name))
|
|
|
|
send_error('Module "%s" does not exist!' % name,
|
|
|
|
status='404 Not Found')
|
2015-03-25 12:22:45 +01:00
|
|
|
|
|
|
|
# try to see if we already have this file...
|
|
|
|
dest_file = os.path.join(hash_dir, filename)
|
2016-10-05 12:43:12 +02:00
|
|
|
old_dir = os.path.join(module_dir, filename, checksum)
|
|
|
|
old_path = os.path.join(old_dir, filename)
|
|
|
|
|
2015-03-25 12:22:45 +01:00
|
|
|
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':
|
2016-10-05 12:43:12 +02:00
|
|
|
if os.path.exists(old_path):
|
|
|
|
# The file had been uploaded at the old path
|
|
|
|
hardlink(old_path, dest_file, username)
|
|
|
|
print 'Available'
|
|
|
|
else:
|
|
|
|
print 'Missing'
|
|
|
|
|
2015-03-25 12:22:45 +01:00
|
|
|
sys.exit(0)
|
2017-07-24 13:12:58 +02:00
|
|
|
elif hash_type == "md5" and config['dist-git'].getboolean('nomd5', fallback=True):
|
2017-07-22 00:54:41 +02:00
|
|
|
send_error('Uploads with md5 are no longer allowed.',
|
|
|
|
status='406 Not Acceptable')
|
2015-03-25 12:22:45 +01:00
|
|
|
|
|
|
|
# check that all directories are in place
|
2016-10-05 12:43:12 +02:00
|
|
|
makedirs(module_dir, username)
|
2015-03-25 12:22:45 +01:00
|
|
|
|
|
|
|
# grab a temporary filename and dump our file in there
|
|
|
|
tempfile.tempdir = module_dir
|
|
|
|
tmpfile = tempfile.mkstemp(checksum)[1]
|
|
|
|
tmpfd = open(tmpfile, 'w')
|
|
|
|
|
|
|
|
# now read the whole file in
|
|
|
|
m = getattr(hashlib, hash_type)()
|
|
|
|
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 checksum of what we got
|
|
|
|
tmpfd.close()
|
|
|
|
check_checksum = m.hexdigest()
|
|
|
|
if checksum != check_checksum:
|
|
|
|
os.unlink(tmpfile)
|
2016-10-05 12:43:12 +02:00
|
|
|
send_error("%s check failed. Received %s instead of %s." %
|
|
|
|
(hash_type.upper(), check_checksum, checksum),
|
|
|
|
status='400 Bad Request')
|
2015-03-25 12:22:45 +01:00
|
|
|
|
|
|
|
# wow, even the checksum matches. make sure full path is valid now
|
2016-10-05 12:43:12 +02:00
|
|
|
makedirs(hash_dir, username)
|
2015-03-25 12:22:45 +01:00
|
|
|
os.rename(tmpfile, dest_file)
|
|
|
|
os.chmod(dest_file, 0644)
|
|
|
|
|
2016-10-05 12:43:12 +02:00
|
|
|
sys.stderr.write('[username=%s] Stored %s (%d bytes)' % (username,
|
|
|
|
dest_file,
|
|
|
|
filesize))
|
|
|
|
print 'File %s size %d %s %s stored OK' % (filename, filesize,
|
|
|
|
hash_type.upper(), checksum)
|
Upload files to new and old paths + remove email
=============
distgit: Upload files to both the new and old path
Currently, the CGI script is set to upload files:
- to the old path if the upload uses md5
- to the new path if the upload uses sha512
The old path is as follows:
/%(srpmname)s/%(filename)s/%(hash)s/%(filename)s
The new path is:
/%(srpmname)s/%(filename)s/%(hashtype)s/%(hash)s/%(filename)s
This was meant to ensure compatibility with current fedpkg which
always downloads from the old path, but will eventually download from
the new path when we move to sha512.
However, working more on this, I now think it would make for a smoother
transition if we instead always stored the files at the new path, but
just hardlinked to the old path if the upload is using md5.
This is what this patch achieves.
With this deployed in production, fedpkg could be patched to try
downloading from the new path, and fallback to the old one if necessary,
which decouples the migration to the new path from the migration to the
new hash.
=============
distgit: Ensure the folder exists
We can't hard link the file if the folder containing the link
destination does not exist.
Hurray for testing in staging!
=============
distgit: os.link fails if the dest already exists
=============
distgit: And so does os.makedirs
=============
distgit: Actually hardlink over the existing source at the old path
Without this, the file could exist at both the old and new path, taking
the space on the disk twice.
This forces a hardlink if the file already existed at the old path.
=============
Remove the lookaside email hook (rely on fedmsg now).
2015-06-03 13:42:57 +02:00
|
|
|
|
|
|
|
# Add the file to the old path, where fedpkg is currently looking for it
|
|
|
|
if hash_type == "md5":
|
2016-10-05 12:43:12 +02:00
|
|
|
hardlink(dest_file, old_path, username)
|
2015-03-25 12:22:45 +01:00
|
|
|
|
|
|
|
# Emit a fedmsg message. Load the config to talk to the fedmsg-relay.
|
|
|
|
try:
|
|
|
|
config = fedmsg.config.load_config([], None)
|
|
|
|
config['active'] = True
|
|
|
|
config['endpoints']['relay_inbound'] = config['relay_inbound']
|
|
|
|
fedmsg.init(name="relay_inbound", cert_prefix="lookaside", **config)
|
|
|
|
|
|
|
|
topic = "lookaside.new"
|
2016-10-05 12:43:12 +02:00
|
|
|
msg = dict(name=name, md5sum=checksum,
|
|
|
|
filename=filename.split('/')[-1], agent=username,
|
|
|
|
path=msgpath)
|
2015-03-25 12:22:45 +01:00
|
|
|
fedmsg.publish(modname="git", topic=topic, msg=msg)
|
|
|
|
except Exception as e:
|
|
|
|
print "Error with fedmsg", str(e)
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2016-10-05 12:43:12 +02:00
|
|
|
try:
|
|
|
|
main()
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
import traceback
|
|
|
|
sys.stderr.write('%s\n' % traceback.format_exc())
|
|
|
|
send_error(str(e))
|