#!/usr/bin/env python # # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # # A script intended to be useful in helping to collect signatures for a # release. This is a pretty rough, and patches are welcome to improve it. # # Some thoughts about future improvement: # * Display of per-file and per-release statistics # * Make use of the python-gpg package (http://code.google.com/p/python-gnupg/) # * Post to IRC when a new signature is collected # - Since we don't want to have a long running bot, perhaps we could # also patch wayita to accept and then echo a privmsg? # * Mail dev@ when somebody submits a successful signature, and include a # comments field which could be included in the mail. # * Use a subversion repository instead of sqlite backend # - no need to re-invent storage and retrieval # - perhaps we could re-use existing CIA/mailer hooks? # import sys, os import sqlite3 def make_config(): 'Output a blank config file' if os.path.exists('config.py'): print "'config.py' already exists!'" sys.exit(1) conf = open('config.py', 'w') conf.write("version = ''\n") conf.write("sigdir = ''\n") conf.write("filesdir = ''\n") conf.close() print "'config.py' generated" def make_db(): 'Initialize a blank database' db = sqlite3.connect('sigs.db') db.execute(''' CREATE TABLE signatures ( keyid TEXT, filename TEXT, signature BLOB, UNIQUE(keyid,filename) ); '''); # This function is web-facing def generate_asc_files(target_dir='.'): fds = {} def _open(filename): if not fds.has_key(filename): fd = open(os.path.join(target_dir, filename + '.asc'), 'w') fds[filename] = fd return fds[filename] db = sqlite3.connect(os.path.join(target_dir, 'sigs.db')) curs = db.cursor() curs.execute('SELECT filename, signature FROM signatures;') for filename, signature in curs: fd = _open(filename) fd.write(signature + "\n") for fd in fds.values(): fd.flush() fd.close() actions = { 'make_config' : make_config, 'make_db' : make_db, 'make_asc' : generate_asc_files, } if __name__ == '__main__': if len(sys.argv) > 1: if sys.argv[1] in actions: actions[sys.argv[1]]() sys.exit(0) # Stuff below this line is the web-facing side # ====================================================================== import cgi import cgitb cgitb.enable() import string, subprocess, re try: sys.path.append(os.path.dirname(sys.argv[0])) import config except: print 'Content-type: text/plain' print print 'Cannot find config file' sys.exit(1) r = re.compile('^\[GNUPG\:\] GOODSIG (\w*) (.*)') def files(): for f in os.listdir(config.filesdir): if config.version in f and (f.endswith('.tar.gz') or f.endswith('.zip') or f.endswith('.tar.bz2')): yield f def ordinal(N): try: return [None, 'first', 'second', 'third', 'fourth', 'fifth', 'sixth'][N] except: # Huh? We only have six files to sign. return "%dth" % N shell_content = ''' Signature collection for Subversion $version

This page is used to collect signatures for the proposed release of Apache Subversion $version.

$content ''' % os.getenv('SCRIPT_NAME') signature_area = '''

Paste one or more signatures in the area below:

Any text not between the BEGIN PGP SIGNATURE and END PGP SIGNATURE lines will be ignored.


''' % os.getenv('SCRIPT_NAME') def split(sigs): lines = [] for line in sigs.split('\n'): if lines or '--BEGIN' in line: lines.append(line) if '--END' in line: yield "\n".join(lines) + "\n" lines = [] def list_signatures(): db = sqlite3.connect(os.path.join(config.sigdir, 'sigs.db')) template = '''

The following signature files are available:

%s

''' lines = "" curs = db.cursor() curs.execute('''SELECT filename, COUNT(*) FROM signatures GROUP BY filename ORDER BY filename''') for filename, count in curs: lines += '%s.asc: %d signature%s
\n' \ % (os.getenv('SCRIPT_NAME'), filename, filename, count, ['s', ''][count == 1]) return (template % lines) + signature_area def save_valid_sig(db, filename, keyid, signature): db.execute('INSERT OR REPLACE INTO signatures VALUES (?,?,?);', (keyid, filename, buffer(signature))) db.commit() generate_asc_files(config.sigdir) def verify_sig_for_file(signature, filename): args = ['gpg', '--logger-fd', '1', '--no-tty', '--status-fd', '2', '--verify', '-', os.path.join(config.filesdir, filename)] gpg = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) gpg.stdin.write(signature) gpg.stdin.close() rc = gpg.wait() output = gpg.stdout.read() status = gpg.stderr.read() if rc: return (False, status + output) lines = status.split('\n') for line in lines: match = r.search(line) if match: keyid = match.group(1) user = match.group(2) return (True, (filename, keyid, user)) def verify_sig(signature): all_failures = "" for filename in files(): (verified, result) = verify_sig_for_file(signature, filename) if verified: return (verified, result) else: all_failures += "%s:\n[[[\n%s]]]\n\n" % (filename, result) return (False, all_failures) def process_sigs(signatures): success = '''

All %d signatures verified!

''' failure = '''

%d of %d signatures failed to verify; details below.

''' c_verified = '''

The signature is verified!

Filename: %s

Key ID: %s

User: %s

This signature has been saved, and will be included as part of the release signatures.

''' c_unverified = '''

The signature was not able to be verified!

Signature:

%s

Reason:

%s

Please talk to the release manager if this is in error.

''' outcomes = [] N_sigs = 0 N_verified = 0 retval = '' # Verify db = sqlite3.connect(os.path.join(config.sigdir, 'sigs.db')) for signature in split(signatures): N_sigs += 1 (verified, result) = verify_sig(signature) outcomes.append((verified, result)) if verified: (filename, keyid, user) = result save_valid_sig(db, filename, keyid, signature) N_verified += 1 # Output header if N_verified == N_sigs: retval += success % N_sigs else: retval += failure % (N_sigs-N_verified, N_sigs) # Output details N = 0 for outcome in outcomes: N += 1 (verified, result) = outcome retval += "

Results for the %s signature

" % ordinal(N) if verified: (filename, keyid, user) = result retval += c_verified % (filename, keyid[-8:], user) else: retval += c_unverified % (signature, result) return retval + signature_area def cat_signatures(basename): # strip '.asc' extension assert basename[:-4] in files() # cat ascfile = os.path.join(config.sigdir, basename) if os.path.exists(ascfile): return (open(ascfile, 'r').read()) def print_content_type(mimetype): print "Content-Type: " + mimetype print def main(): form = cgi.FieldStorage() pathinfo = os.getenv('PATH_INFO') # default value, to be changed below content = signature_area if 'signatures' in form: content = process_sigs(form['signatures'].value) elif pathinfo and pathinfo[1:]: basename = pathinfo.split('/')[-1] if basename == 'list': content = list_signatures() elif basename[:-4] in files(): # early exit; bypass 'content' entirely print_content_type('text/plain') print cat_signatures(basename) return # These are "global" values, not specific to our action. mapping = { 'version' : config.version, 'content' : content, } print_content_type('text/html') template = string.Template(shell_content) print template.safe_substitute(mapping) if __name__ == '__main__': main()