| #!/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 = ''' |
| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" |
| "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> |
| <html> |
| <head> |
| <title>Signature collection for Subversion $version</title> |
| </head> |
| <body style="font-size: 14pt; text-align: justify; |
| background-color: #f0f0f0; padding: 0 5%%"> |
| <p>This page is used to collect <a href="%s/list">signatures</a> for the |
| proposed release of Apache Subversion $version.</p> |
| $content |
| </body> |
| </html> |
| ''' % os.getenv('SCRIPT_NAME') |
| |
| signature_area = ''' |
| <hr/> |
| <form method="post" action="%s"> |
| <p>Paste one or more signatures in the area below:<br/> |
| <textarea name="signatures" rows="20" cols="80"></textarea> |
| </p> |
| <input type="submit" value="Submit" /> |
| <p>Any text not between the <tt>BEGIN PGP SIGNATURE</tt> |
| and <tt>END PGP SIGNATURE</tt> lines will be ignored.</p> |
| </form> |
| <hr/> |
| ''' % 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 = ''' |
| <hr/> |
| <p>The following signature files are available:</p> |
| <p>%s</p> |
| ''' |
| |
| lines = "" |
| curs = db.cursor() |
| curs.execute('''SELECT filename, COUNT(*) FROM signatures |
| GROUP BY filename ORDER BY filename''') |
| for filename, count in curs: |
| lines += '<a href="%s/%s.asc">%s.asc</a>: %d signature%s<br/>\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 = ''' |
| <p style="color: green;">All %d signatures verified!</p> |
| ''' |
| failure = ''' |
| <p style="color: red;">%d of %d signatures failed to verify; details below.</p> |
| ''' |
| c_verified = ''' |
| <p style="color: green;">The signature is verified!</p> |
| <p>Filename: <code>%s</code></p> |
| <p>Key ID: <code>%s</code></p> |
| <p>User: <code>%s</code></p> |
| <p>This signature has been saved, and will be included as part of the |
| release signatures.</p> |
| ''' |
| c_unverified = ''' |
| <p style="color: red;">The signature was not able to be verified!</p> |
| <p>Signature: <pre>%s</pre></p> |
| <p>Reason:</p><pre>%s</pre> |
| <p>Please talk to the release manager if this is in error.</p> |
| ''' |
| |
| 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 += "<h1>Results for the %s signature</h1>" % 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() |