4 # Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org>
6 ################################################################################
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 ################################################################################
24 import codecs, commands, email.Header, os, pwd, re, select, socket, shutil, \
25 sys, tempfile, traceback
29 ################################################################################
31 re_comments = re.compile(r"\#.*")
32 re_no_epoch = re.compile(r"^\d+\:")
33 re_no_revision = re.compile(r"-[^-]+$")
34 re_arch_from_filename = re.compile(r"/binary-[^/]+/")
35 re_extract_src_version = re.compile (r"(\S+)\s*\((.*)\)")
36 re_isadeb = re.compile (r"(.+?)_(.+?)_(.+)\.u?deb$")
37 re_issource = re.compile (r"(.+)_(.+?)\.(orig\.tar\.gz|diff\.gz|tar\.gz|dsc)$")
39 re_single_line_field = re.compile(r"^(\S*)\s*:\s*(.*)")
40 re_multi_line_field = re.compile(r"^\s(.*)")
41 re_taint_free = re.compile(r"^[-+~/\.\w]+$")
43 re_parse_maintainer = re.compile(r"^\s*(\S.*\S)\s*\<([^\>]+)\>")
45 re_srchasver = re.compile(r"^(\S+)\s+\((\S+)\)$")
47 changes_parse_error_exc = "Can't parse line in .changes file"
48 invalid_dsc_format_exc = "Invalid .dsc file"
49 nk_format_exc = "Unknown Format: in .changes file"
50 no_files_exc = "No Files: field in .dsc or .changes file."
51 cant_open_exc = "Can't open file"
52 unknown_hostname_exc = "Unknown hostname"
53 cant_overwrite_exc = "Permission denied; can't overwrite existent file."
54 file_exists_exc = "Destination file exists"
55 sendmail_failed_exc = "Sendmail invocation failed"
56 tried_too_hard_exc = "Tried too hard to find a free filename."
58 default_config = "/etc/dak/dak.conf"
59 default_apt_config = "/etc/dak/apt.conf"
61 ################################################################################
63 class Error(Exception):
64 """Base class for exceptions in this module."""
67 class ParseMaintError(Error):
68 """Exception raised for errors in parsing a maintainer field.
71 message -- explanation of the error
74 def __init__(self, message):
76 self.message = message
78 ################################################################################
80 def open_file(filename, mode='r'):
82 f = open(filename, mode)
84 raise cant_open_exc, filename
87 ################################################################################
89 def our_raw_input(prompt=""):
91 sys.stdout.write(prompt)
97 sys.stderr.write("\nUser interrupt (^D).\n")
100 ################################################################################
102 def extract_component_from_section(section):
105 if section.find('/') != -1:
106 component = section.split('/')[0]
108 # Expand default component
110 if Cnf.has_key("Component::%s" % section):
115 return (section, component)
117 ################################################################################
119 def parse_changes(filename, signing_rules=0):
120 """Parses a changes file and returns a dictionary where each field is a
121 key. The mandatory first argument is the filename of the .changes
124 signing_rules is an optional argument:
126 o If signing_rules == -1, no signature is required.
127 o If signing_rules == 0 (the default), a signature is required.
128 o If signing_rules == 1, it turns on the same strict format checking
131 The rules for (signing_rules == 1)-mode are:
133 o The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
134 followed by any PGP header data and must end with a blank line.
136 o The data section must end with a blank line and must be followed by
137 "-----BEGIN PGP SIGNATURE-----".
143 changes_in = open_file(filename)
144 lines = changes_in.readlines()
147 raise changes_parse_error_exc, "[Empty changes file]"
149 # Reindex by line number so we can easily verify the format of
155 indexed_lines[index] = line[:-1]
159 num_of_lines = len(indexed_lines.keys())
162 while index < num_of_lines:
164 line = indexed_lines[index]
166 if signing_rules == 1:
168 if index > num_of_lines:
169 raise invalid_dsc_format_exc, index
170 line = indexed_lines[index]
171 if not line.startswith("-----BEGIN PGP SIGNATURE"):
172 raise invalid_dsc_format_exc, index
177 if line.startswith("-----BEGIN PGP SIGNATURE"):
179 if line.startswith("-----BEGIN PGP SIGNED MESSAGE"):
181 if signing_rules == 1:
182 while index < num_of_lines and line != "":
184 line = indexed_lines[index]
186 # If we're not inside the signed data, don't process anything
187 if signing_rules >= 0 and not inside_signature:
189 slf = re_single_line_field.match(line)
191 field = slf.groups()[0].lower()
192 changes[field] = slf.groups()[1]
196 changes[field] += '\n'
198 mlf = re_multi_line_field.match(line)
201 raise changes_parse_error_exc, "'%s'\n [Multi-line field continuing on from nothing?]" % (line)
202 if first == 1 and changes[field] != "":
203 changes[field] += '\n'
205 changes[field] += mlf.groups()[0] + '\n'
209 if signing_rules == 1 and inside_signature:
210 raise invalid_dsc_format_exc, index
213 changes["filecontents"] = "".join(lines)
215 if changes.has_key("source"):
216 # Strip the source version in brackets from the source field,
217 # put it in the "source-version" field instead.
218 srcver = re_srchasver.search(changes["source"])
220 changes["source"] = srcver.group(1)
221 changes["source-version"] = srcver.group(2)
224 raise changes_parse_error_exc, error
228 ################################################################################
230 # Determine what parts in a .changes are NEW
232 def determine_new (changes, files, projectB, warn=1):
235 # Build up a list of potentially new things
236 for file in files.keys():
238 # Skip byhand elements
239 if f["type"] == "byhand":
242 priority = f["priority"]
243 section = f["section"]
245 component = f["component"]
249 if not new.has_key(pkg):
251 new[pkg]["priority"] = priority
252 new[pkg]["section"] = section
253 new[pkg]["type"] = type
254 new[pkg]["component"] = component
255 new[pkg]["files"] = []
257 old_type = new[pkg]["type"]
259 # source gets trumped by deb or udeb
260 if old_type == "dsc":
261 new[pkg]["priority"] = priority
262 new[pkg]["section"] = section
263 new[pkg]["type"] = type
264 new[pkg]["component"] = component
265 new[pkg]["files"].append(file)
266 if f.has_key("othercomponents"):
267 new[pkg]["othercomponents"] = f["othercomponents"]
269 for suite in changes["suite"].keys():
270 suite_id = database.get_suite_id(suite)
271 for pkg in new.keys():
272 component_id = database.get_component_id(new[pkg]["component"])
273 type_id = database.get_override_type_id(new[pkg]["type"])
274 q = projectB.query("SELECT package FROM override WHERE package = '%s' AND suite = %s AND component = %s AND type = %s" % (pkg, suite_id, component_id, type_id))
277 for file in new[pkg]["files"]:
278 if files[file].has_key("new"):
279 del files[file]["new"]
283 if changes["suite"].has_key("stable"):
284 print "WARNING: overrides will be added for stable!"
285 if changes["suite"].has_key("oldstable"):
286 print "WARNING: overrides will be added for OLDstable!"
287 for pkg in new.keys():
288 if new[pkg].has_key("othercomponents"):
289 print "WARNING: %s already present in %s distribution." % (pkg, new[pkg]["othercomponents"])
293 ################################################################################
297 if f.has_key("dbtype"):
299 elif f["type"] in [ "orig.tar.gz", "orig.tar.bz2", "tar.gz", "tar.bz2", "diff.gz", "diff.bz2", "dsc" ]:
302 fubar("invalid type (%s) for new. Dazed, confused and sure as heck not continuing." % (type))
304 # Validate the override type
305 type_id = database.get_override_type_id(type)
307 fubar("invalid type (%s) for new. Say wha?" % (type))
311 ################################################################################
313 # check if section/priority values are valid
315 def check_valid (new):
316 for pkg in new.keys():
317 section = new[pkg]["section"]
318 priority = new[pkg]["priority"]
319 type = new[pkg]["type"]
320 new[pkg]["section id"] = database.get_section_id(section)
321 new[pkg]["priority id"] = database.get_priority_id(new[pkg]["priority"])
323 di = section.find("debian-installer") != -1
324 if (di and type != "udeb") or (not di and type == "udeb"):
325 new[pkg]["section id"] = -1
326 if (priority == "source" and type != "dsc") or \
327 (priority != "source" and type == "dsc"):
328 new[pkg]["priority id"] = -1
330 ################################################################################
332 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
334 def build_file_list(changes, is_a_dsc=0):
337 # Make sure we have a Files: field to parse...
338 if not changes.has_key("files"):
341 # Make sure we recognise the format of the Files: field
342 format = changes.get("format", "")
344 format = float(format)
345 if not is_a_dsc and (format < 1.5 or format > 2.0):
346 raise nk_format_exc, format
348 # Parse each entry/line:
349 for i in changes["files"].split('\n'):
353 section = priority = ""
356 (md5, size, name) = s
358 (md5, size, section, priority, name) = s
360 raise changes_parse_error_exc, i
367 (section, component) = extract_component_from_section(section)
369 files[name] = Dict(md5sum=md5, size=size, section=section,
370 priority=priority, component=component)
374 ################################################################################
376 def force_to_utf8(s):
377 """Forces a string to UTF-8. If the string isn't already UTF-8,
378 it's assumed to be ISO-8859-1."""
383 latin1_s = unicode(s,'iso8859-1')
384 return latin1_s.encode('utf-8')
386 def rfc2047_encode(s):
387 """Encodes a (header) string per RFC2047 if necessary. If the
388 string is neither ASCII nor UTF-8, it's assumed to be ISO-8859-1."""
390 codecs.lookup('ascii')[1](s)
395 codecs.lookup('utf-8')[1](s)
396 h = email.Header.Header(s, 'utf-8', 998)
399 h = email.Header.Header(s, 'iso-8859-1', 998)
402 ################################################################################
404 # <Culus> 'The standard sucks, but my tool is supposed to interoperate
405 # with it. I know - I'll fix the suckage and make things
408 def fix_maintainer (maintainer):
409 """Parses a Maintainer or Changed-By field and returns:
410 (1) an RFC822 compatible version,
411 (2) an RFC2047 compatible version,
415 The name is forced to UTF-8 for both (1) and (3). If the name field
416 contains '.' or ',' (as allowed by Debian policy), (1) and (2) are
417 switched to 'email (name)' format."""
418 maintainer = maintainer.strip()
420 return ('', '', '', '')
422 if maintainer.find("<") == -1:
425 elif (maintainer[0] == "<" and maintainer[-1:] == ">"):
426 email = maintainer[1:-1]
429 m = re_parse_maintainer.match(maintainer)
431 raise ParseMaintError, "Doesn't parse as a valid Maintainer field."
435 # Get an RFC2047 compliant version of the name
436 rfc2047_name = rfc2047_encode(name)
438 # Force the name to be UTF-8
439 name = force_to_utf8(name)
441 if name.find(',') != -1 or name.find('.') != -1:
442 rfc822_maint = "%s (%s)" % (email, name)
443 rfc2047_maint = "%s (%s)" % (email, rfc2047_name)
445 rfc822_maint = "%s <%s>" % (name, email)
446 rfc2047_maint = "%s <%s>" % (rfc2047_name, email)
448 if email.find("@") == -1 and email.find("buildd_") != 0:
449 raise ParseMaintError, "No @ found in email address part."
451 return (rfc822_maint, rfc2047_maint, name, email)
453 ################################################################################
455 # sendmail wrapper, takes _either_ a message string or a file as arguments
456 def send_mail (message, filename=""):
457 # If we've been passed a string dump it into a temporary file
459 filename = tempfile.mktemp()
460 fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700)
461 os.write (fd, message)
465 (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
467 raise sendmail_failed_exc, output
469 # Clean up any temporary files
473 ################################################################################
475 def poolify (source, component):
478 if source[:3] == "lib":
479 return component + source[:4] + '/' + source + '/'
481 return component + source[:1] + '/' + source + '/'
483 ################################################################################
485 def move (src, dest, overwrite = 0, perms = 0664):
486 if os.path.exists(dest) and os.path.isdir(dest):
489 dest_dir = os.path.dirname(dest)
490 if not os.path.exists(dest_dir):
491 umask = os.umask(00000)
492 os.makedirs(dest_dir, 02775)
494 #print "Moving %s to %s..." % (src, dest)
495 if os.path.exists(dest) and os.path.isdir(dest):
496 dest += '/' + os.path.basename(src)
497 # Don't overwrite unless forced to
498 if os.path.exists(dest):
500 fubar("Can't move %s to %s - file already exists." % (src, dest))
502 if not os.access(dest, os.W_OK):
503 fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
504 shutil.copy2(src, dest)
505 os.chmod(dest, perms)
508 def copy (src, dest, overwrite = 0, perms = 0664):
509 if os.path.exists(dest) and os.path.isdir(dest):
512 dest_dir = os.path.dirname(dest)
513 if not os.path.exists(dest_dir):
514 umask = os.umask(00000)
515 os.makedirs(dest_dir, 02775)
517 #print "Copying %s to %s..." % (src, dest)
518 if os.path.exists(dest) and os.path.isdir(dest):
519 dest += '/' + os.path.basename(src)
520 # Don't overwrite unless forced to
521 if os.path.exists(dest):
523 raise file_exists_exc
525 if not os.access(dest, os.W_OK):
526 raise cant_overwrite_exc
527 shutil.copy2(src, dest)
528 os.chmod(dest, perms)
530 ################################################################################
533 res = socket.gethostbyaddr(socket.gethostname())
534 database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname")
535 if database_hostname:
536 return database_hostname
540 def which_conf_file ():
541 res = socket.gethostbyaddr(socket.gethostname())
542 if Cnf.get("Config::" + res[0] + "::DakConfig"):
543 return Cnf["Config::" + res[0] + "::DakConfig"]
545 return default_config
547 def which_apt_conf_file ():
548 res = socket.gethostbyaddr(socket.gethostname())
549 if Cnf.get("Config::" + res[0] + "::AptConfig"):
550 return Cnf["Config::" + res[0] + "::AptConfig"]
552 return default_apt_config
554 ################################################################################
556 # Escape characters which have meaning to SQL's regex comparison operator ('~')
557 # (woefully incomplete)
560 s = s.replace('+', '\\\\+')
561 s = s.replace('.', '\\\\.')
564 ################################################################################
566 # Perform a substition of template
567 def TemplateSubst(map, filename):
568 file = open_file(filename)
569 template = file.read()
571 template = template.replace(x,map[x])
575 ################################################################################
577 def fubar(msg, exit_code=1):
578 sys.stderr.write("E: %s\n" % (msg))
582 sys.stderr.write("W: %s\n" % (msg))
584 ################################################################################
586 # Returns the user name with a laughable attempt at rfc822 conformancy
587 # (read: removing stray periods).
589 return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
591 ################################################################################
601 return ("%d%s" % (c, t))
603 ################################################################################
605 def cc_fix_changes (changes):
606 o = changes.get("architecture", "")
608 del changes["architecture"]
609 changes["architecture"] = {}
611 changes["architecture"][j] = 1
613 # Sort by source name, source version, 'have source', and then by filename
614 def changes_compare (a, b):
616 a_changes = parse_changes(a)
621 b_changes = parse_changes(b)
625 cc_fix_changes (a_changes)
626 cc_fix_changes (b_changes)
628 # Sort by source name
629 a_source = a_changes.get("source")
630 b_source = b_changes.get("source")
631 q = cmp (a_source, b_source)
635 # Sort by source version
636 a_version = a_changes.get("version", "0")
637 b_version = b_changes.get("version", "0")
638 q = apt_pkg.VersionCompare(a_version, b_version)
642 # Sort by 'have source'
643 a_has_source = a_changes["architecture"].get("source")
644 b_has_source = b_changes["architecture"].get("source")
645 if a_has_source and not b_has_source:
647 elif b_has_source and not a_has_source:
650 # Fall back to sort by filename
653 ################################################################################
655 def find_next_free (dest, too_many=100):
658 while os.path.exists(dest) and extra < too_many:
659 dest = orig_dest + '.' + repr(extra)
661 if extra >= too_many:
662 raise tried_too_hard_exc
665 ################################################################################
667 def result_join (original, sep = '\t'):
669 for i in xrange(len(original)):
670 if original[i] == None:
673 list.append(original[i])
674 return sep.join(list)
676 ################################################################################
678 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
680 for line in str.split('\n'):
682 if line or include_blank_lines:
683 out += "%s%s\n" % (prefix, line)
684 # Strip trailing new line
689 ################################################################################
691 def validate_changes_file_arg(filename, require_changes=1):
692 """'filename' is either a .changes or .dak file. If 'filename' is a
693 .dak file, it's changed to be the corresponding .changes file. The
694 function then checks if the .changes file a) exists and b) is
695 readable and returns the .changes filename if so. If there's a
696 problem, the next action depends on the option 'require_changes'
699 o If 'require_changes' == -1, errors are ignored and the .changes
700 filename is returned.
701 o If 'require_changes' == 0, a warning is given and 'None' is returned.
702 o If 'require_changes' == 1, a fatal error is raised.
706 orig_filename = filename
707 if filename.endswith(".dak"):
708 filename = filename[:-4]+".changes"
710 if not filename.endswith(".changes"):
711 error = "invalid file type; not a changes file"
713 if not os.access(filename,os.R_OK):
714 if os.path.exists(filename):
715 error = "permission denied"
717 error = "file not found"
720 if require_changes == 1:
721 fubar("%s: %s." % (orig_filename, error))
722 elif require_changes == 0:
723 warn("Skipping %s - %s" % (orig_filename, error))
725 else: # We only care about the .dak file
730 ################################################################################
733 return (arch != "source" and arch != "all")
735 ################################################################################
737 def join_with_commas_and(list):
738 if len(list) == 0: return "nothing"
739 if len(list) == 1: return list[0]
740 return ", ".join(list[:-1]) + " and " + list[-1]
742 ################################################################################
747 (pkg, version, constraint) = atom
749 pp_dep = "%s (%s %s)" % (pkg, constraint, version)
752 pp_deps.append(pp_dep)
753 return " |".join(pp_deps)
755 ################################################################################
760 ################################################################################
762 # Handle -a, -c and -s arguments; returns them as SQL constraints
763 def parse_args(Options):
767 for suite in split_args(Options["Suite"]):
768 suite_id = database.get_suite_id(suite)
770 warn("suite '%s' not recognised." % (suite))
772 suite_ids_list.append(suite_id)
774 con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
776 fubar("No valid suite given.")
781 if Options["Component"]:
782 component_ids_list = []
783 for component in split_args(Options["Component"]):
784 component_id = database.get_component_id(component)
785 if component_id == -1:
786 warn("component '%s' not recognised." % (component))
788 component_ids_list.append(component_id)
789 if component_ids_list:
790 con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
792 fubar("No valid component given.")
796 # Process architecture
797 con_architectures = ""
798 if Options["Architecture"]:
801 for architecture in split_args(Options["Architecture"]):
802 if architecture == "source":
805 architecture_id = database.get_architecture_id(architecture)
806 if architecture_id == -1:
807 warn("architecture '%s' not recognised." % (architecture))
809 arch_ids_list.append(architecture_id)
811 con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
814 fubar("No valid architecture given.")
818 return (con_suites, con_architectures, con_components, check_source)
820 ################################################################################
822 # Inspired(tm) by Bryn Keller's print_exc_plus (See
823 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)
826 tb = sys.exc_info()[2]
835 traceback.print_exc()
837 print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
838 frame.f_code.co_filename,
840 for key, value in frame.f_locals.items():
841 print "\t%20s = " % key,
845 print "<unable to print>"
847 ################################################################################
849 def try_with_debug(function):
857 ################################################################################
859 # Function for use in sorting lists of architectures.
860 # Sorts normally except that 'source' dominates all others.
862 def arch_compare_sw (a, b):
863 if a == "source" and b == "source":
872 ################################################################################
874 # Split command line arguments which can be separated by either commas
875 # or whitespace. If dwim is set, it will complain about string ending
876 # in comma since this usually means someone did 'dak ls -a i386, m68k
877 # foo' or something and the inevitable confusion resulting from 'm68k'
878 # being treated as an argument is undesirable.
880 def split_args (s, dwim=1):
881 if s.find(",") == -1:
884 if s[-1:] == "," and dwim:
885 fubar("split_args: found trailing comma, spurious space maybe?")
888 ################################################################################
890 def Dict(**dict): return dict
892 ########################################
894 # Our very own version of commands.getouputstatus(), hacked to support
896 def gpgv_get_status_output(cmd, status_read, status_write):
897 cmd = ['/bin/sh', '-c', cmd]
898 p2cread, p2cwrite = os.pipe()
899 c2pread, c2pwrite = os.pipe()
900 errout, errin = os.pipe()
910 for i in range(3, 256):
911 if i != status_write:
917 os.execvp(cmd[0], cmd)
923 os.dup2(c2pread, c2pwrite)
924 os.dup2(errout, errin)
928 i, o, e = select.select([c2pwrite, errin, status_read], [], [])
931 r = os.read(fd, 8196)
934 if fd == c2pwrite or fd == errin:
936 elif fd == status_read:
939 fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
941 pid, exit_status = os.waitpid(pid, 0)
943 os.close(status_write)
944 os.close(status_read)
954 return output, status, exit_status
956 ################################################################################
958 def process_gpgv_output(status):
959 # Process the status-fd output
962 for line in status.split('\n'):
968 internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
970 (gnupg, keyword) = split[:2]
971 if gnupg != "[GNUPG:]":
972 internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
975 if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
976 internal_error += "found duplicate status token ('%s').\n" % (keyword)
979 keywords[keyword] = args
981 return (keywords, internal_error)
983 ################################################################################
985 def retrieve_key (filename, keyserver=None, keyring=None):
986 """Retrieve the key that signed 'filename' from 'keyserver' and
987 add it to 'keyring'. Returns nothing on success, or an error message
990 # Defaults for keyserver and keyring
992 keyserver = Cnf["Dinstall::KeyServer"]
994 keyring = Cnf.ValueList("Dinstall::GPGKeyring")[0]
996 # Ensure the filename contains no shell meta-characters or other badness
997 if not re_taint_free.match(filename):
998 return "%s: tainted filename" % (filename)
1000 # Invoke gpgv on the file
1001 status_read, status_write = os.pipe();
1002 cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
1003 (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
1005 # Process the status-fd output
1006 (keywords, internal_error) = process_gpgv_output(status)
1008 return internal_error
1010 if not keywords.has_key("NO_PUBKEY"):
1011 return "didn't find expected NO_PUBKEY in gpgv status-fd output"
1013 fingerprint = keywords["NO_PUBKEY"][0]
1014 # XXX - gpg sucks. You can't use --secret-keyring=/dev/null as
1015 # it'll try to create a lockfile in /dev. A better solution might
1016 # be a tempfile or something.
1017 cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
1018 % (Cnf["Dinstall::SigningKeyring"])
1019 cmd += " --keyring %s --keyserver %s --recv-key %s" \
1020 % (keyring, keyserver, fingerprint)
1021 (result, output) = commands.getstatusoutput(cmd)
1023 return "'%s' failed with exit code %s" % (cmd, result)
1027 ################################################################################
1029 def gpg_keyring_args(keyrings=None):
1031 keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1033 return " ".join(["--keyring %s" % x for x in keyrings])
1035 ################################################################################
1037 def check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
1038 """Check the signature of a file and return the fingerprint if the
1039 signature is valid or 'None' if it's not. The first argument is the
1040 filename whose signature should be checked. The second argument is a
1041 reject function and is called when an error is found. The reject()
1042 function must allow for two arguments: the first is the error message,
1043 the second is an optional prefix string. It's possible for reject()
1044 to be called more than once during an invocation of check_signature().
1045 The third argument is optional and is the name of the files the
1046 detached signature applies to. The fourth argument is optional and is
1047 a *list* of keyrings to use. 'autofetch' can either be None, True or
1048 False. If None, the default behaviour specified in the config will be
1051 # Ensure the filename contains no shell meta-characters or other badness
1052 if not re_taint_free.match(sig_filename):
1053 reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
1056 if data_filename and not re_taint_free.match(data_filename):
1057 reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
1061 keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1063 # Autofetch the signing key if that's enabled
1064 if autofetch == None:
1065 autofetch = Cnf.get("Dinstall::KeyAutoFetch")
1067 error_msg = retrieve_key(sig_filename)
1072 # Build the command line
1073 status_read, status_write = os.pipe();
1074 cmd = "gpgv --status-fd %s %s %s %s" % (
1075 status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)
1077 # Invoke gpgv on the file
1078 (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
1080 # Process the status-fd output
1081 (keywords, internal_error) = process_gpgv_output(status)
1083 # If we failed to parse the status-fd output, let's just whine and bail now
1085 reject("internal error while performing signature check on %s." % (sig_filename))
1086 reject(internal_error, "")
1087 reject("Please report the above errors to the Archive maintainers by replying to this mail.", "")
1091 # Now check for obviously bad things in the processed output
1092 if keywords.has_key("KEYREVOKED"):
1093 reject("The key used to sign %s has been revoked." % (sig_filename))
1095 if keywords.has_key("BADSIG"):
1096 reject("bad signature on %s." % (sig_filename))
1098 if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1099 reject("failed to check signature on %s." % (sig_filename))
1101 if keywords.has_key("NO_PUBKEY"):
1102 args = keywords["NO_PUBKEY"]
1105 reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
1107 if keywords.has_key("BADARMOR"):
1108 reject("ASCII armour of signature was corrupt in %s." % (sig_filename))
1110 if keywords.has_key("NODATA"):
1111 reject("no signature found in %s." % (sig_filename))
1113 if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1114 reject("The key (0x%s) used to sign %s has expired." % (key, sig_filename))
1120 # Next check gpgv exited with a zero return code
1122 reject("gpgv failed while checking %s." % (sig_filename))
1124 reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1126 reject(prefix_multi_line_string(output, " [GPG output:] "), "")
1129 # Sanity check the good stuff we expect
1130 if not keywords.has_key("VALIDSIG"):
1131 reject("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
1134 args = keywords["VALIDSIG"]
1136 reject("internal error while checking signature on %s." % (sig_filename))
1139 fingerprint = args[0]
1140 if not keywords.has_key("GOODSIG"):
1141 reject("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
1143 if not keywords.has_key("SIG_ID"):
1144 reject("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
1147 # Finally ensure there's not something we don't recognise
1148 known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
1149 SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1150 NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="")
1152 for keyword in keywords.keys():
1153 if not known_keywords.has_key(keyword):
1154 reject("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
1162 ################################################################################
1164 # Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603
1166 def wrap(paragraph, max_length, prefix=""):
1170 words = paragraph.split()
1173 word_size = len(word)
1174 if word_size > max_length:
1176 s += line + '\n' + prefix
1177 s += word + '\n' + prefix
1180 new_length = len(line) + word_size + 1
1181 if new_length > max_length:
1182 s += line + '\n' + prefix
1195 ################################################################################
1197 # Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
1198 # Returns fixed 'src'
1199 def clean_symlink (src, dest, root):
1200 src = src.replace(root, '', 1)
1201 dest = dest.replace(root, '', 1)
1202 dest = os.path.dirname(dest)
1203 new_src = '../' * len(dest.split('/'))
1204 return new_src + src
1206 ################################################################################
1208 def temp_filename(directory=None, dotprefix=None, perms=0700):
1209 """Return a secure and unique filename by pre-creating it.
1210 If 'directory' is non-null, it will be the directory the file is pre-created in.
1211 If 'dotprefix' is non-null, the filename will be prefixed with a '.'."""
1214 old_tempdir = tempfile.tempdir
1215 tempfile.tempdir = directory
1217 filename = tempfile.mktemp()
1220 filename = "%s/.%s" % (os.path.dirname(filename), os.path.basename(filename))
1221 fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, perms)
1225 tempfile.tempdir = old_tempdir
1229 ################################################################################
1233 Cnf = apt_pkg.newConfiguration()
1234 apt_pkg.ReadConfigFileISC(Cnf,default_config)
1236 if which_conf_file() != default_config:
1237 apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1239 ################################################################################