use /usr/bin/install instead of ginstall
[unleashed-userland.git] / tools / userland-fetch
bloba88791520513db92fcf39ca5641ef67e3c2418f8
1 #!/usr/bin/python2.7
3 # CDDL HEADER START
5 # The contents of this file are subject to the terms of the
6 # Common Development and Distribution License (the "License").
7 # You may not use this file except in compliance with the License.
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 # or http://www.opensolaris.org/os/licensing.
11 # See the License for the specific language governing permissions
12 # and limitations under the License.
14 # When distributing Covered Code, include this CDDL HEADER in each
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 # If applicable, add the following below this CDDL HEADER, with the
17 # fields enclosed by brackets "[]" replaced with your own identifying
18 # information: Portions Copyright [yyyy] [name of copyright owner]
20 # CDDL HEADER END
22 # Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
25 # fetch.py - a file download utility
27 #  A simple program similiar to wget(1), but handles local file copy, ignores
28 #  directories, and verifies file hashes.
31 import errno
32 import os
33 import sys
34 import shutil
35 import subprocess
36 import re
37 import gzip
38 import bz2
39 from urllib import splittype
40 from urllib2 import urlopen
41 from urllib2 import Request
42 import hashlib
45 def printIOError(e, txt):
46     """ Function to decode and print IOError type exception """
47     print "I/O Error: " + txt + ": "
48     try:
49         (code, message) = e
50         print str(message) + " (" + str(code) + ")"
51     except:
52         print str(e)
55 def validate_signature(path, signature):
56     """Given paths to a file and a detached PGP signature, verify that
57     the signature is valid for the file.  Current configuration allows for
58     unrecognized keys to be downloaded as necessary."""
60     # Find the root of the repo so that we can point GnuPG at the right
61     # configuration and keyring.
62     proc = subprocess.Popen(["git", "rev-parse", "--show-toplevel"], stdout=subprocess.PIPE)
63     proc.wait()
64     if proc.returncode != 0:
65         return False
66     out, err = proc.communicate()
67     gpgdir = os.path.join(out.strip(), "tools", ".gnupg")
69     # Skip the permissions warning: none of the information here is private,
70     # so not having to worry about getting git keeping the directory
71     # unreadable is just simplest.
72     try:
73         proc = subprocess.Popen(["gpg2", "--verify",
74                                  "--no-permission-warning", "--homedir", gpgdir, signature,
75                                  path], stdin=open("/dev/null"),
76                                 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
77     except OSError as e:
78         # If the executable simply couldn't be found, just skip the
79         # validation.
80         if e.errno == errno.ENOENT:
81             return False
82         raise
84     proc.wait()
85     if proc.returncode != 0:
86         # Only print GnuPG's output when there was a problem.
87         print proc.stdout.read()
88         return False
89     return True
92 def validate(file, hash):
93     """Given a file-like object and a hash string, verify that the hash
94     matches the file contents."""
96     try:
97         algorithm, hashvalue = hash.split(':')
98     except:
99         algorithm = "sha256"
101     # force migration away from sha1
102     if algorithm == "sha1":
103         algorithm = "sha256"
105     try:
106         m = hashlib.new(algorithm)
107     except ValueError:
108         return False
110     while True:
111         try:
112             block = file.read()
113         except IOError, err:
114             print str(err),
115             break
117         m.update(block)
118         if block == '':
119             break
121     return "%s:%s" % (algorithm, m.hexdigest())
124 def validate_container(filename, hash):
125     """Given a file path and a hash string, verify that the hash matches the
126     file contents."""
128     try:
129         file = open(filename, 'r')
130     except IOError as e:
131         printIOError(e, "Can't open file " + filename)
132         return False
133     return validate(file, hash)
136 def validate_payload(filename, hash):
137     """Given a file path and a hash string, verify that the hash matches the
138     payload (uncompressed content) of the file."""
140     expr_bz = re.compile('.+\.bz2$', re.IGNORECASE)
141     expr_gz = re.compile('.+\.gz$', re.IGNORECASE)
142     expr_tgz = re.compile('.+\.tgz$', re.IGNORECASE)
144     try:
145         if expr_bz.match(filename):
146             file = bz2.BZ2File(filename, 'r')
147         elif expr_gz.match(filename):
148             file = gzip.GzipFile(filename, 'r')
149         elif expr_tgz.match(filename):
150             file = gzip.GzipFile(filename, 'r')
151         else:
152             return False
153     except IOError as e:
154         printIOError(e, "Can't open archive " + filename)
155         return False
156     return validate(file, hash)
159 def download(url, filename=None, user_agent_arg=None, quiet=None):
160     """Download the content at the given URL to the given filename
161     (defaulting to the basename of the URL if not given.  If 'quiet' is
162     True, throw away any error messages.  Returns the name of the file to
163     which the content was donloaded."""
165     src = None
167     try:
168         req = Request(url)
169         if user_agent_arg is not None:
170             req.add_header("User-Agent", user_agent_arg)
171         src = urlopen(req)
172     except IOError as e:
173         if not quiet:
174             printIOError(e, "Can't open url " + url)
175         return None
177     # 3xx, 4xx and 5xx (f|ht)tp codes designate unsuccessfull action
178     if src.getcode() and (3 <= int(src.getcode() / 100) <= 5):
179         if not quiet:
180             print "Error code: " + str(src.getcode())
181         return None
183     if filename is None:
184         filename = src.geturl().split('/')[-1]
186     try:
187         dst = open(filename, 'wb')
188     except IOError as e:
189         if not quiet:
190             printIOError(e, "Can't open file " + filename + " for writing")
191         src.close()
192         return None
194     while True:
195         block = src.read()
196         if block == '':
197             break
198         dst.write(block)
200     src.close()
201     dst.close()
203     # return the name of the file that we downloaded the data to.
204     return filename
207 def download_paths(search, filename, url):
208     """Returns a list of URLs where the file 'filename' might be found,
209     using 'url', 'search', and $DOWNLOAD_SEARCH_PATH as places to look.
211     If 'filename' is None, then the list will simply contain 'url'."""
213     urls = list()
215     if filename is not None:
216         tmp = os.getenv('DOWNLOAD_SEARCH_PATH')
217         if tmp:
218             search += tmp.split(' ')
220         file = os.path.basename(filename)
222         urls = [base + '/' + file for base in search]
224         # filename should always be first
225         if filename in urls:
226             urls.remove(filename)
227         urls.insert(0, filename)
229     # command line url is a fallback, so it's last
230     if url is not None and url not in urls:
231         urls.append(url)
233     return urls
236 def download_from_paths(search_list, file_arg, url, link_arg, quiet=False):
237     """Attempts to download a file from a number of possible locations.
238     Generates a list of paths where the file ends up on the local
239     filesystem.  This is a generator because while a download might be
240     successful, the signature or hash may not validate, and the caller may
241     want to try again from the next location.  The 'link_arg' argument is a
242     boolean which, when True, specifies that if the source is not a remote
243     URL and not already found where it should be, to make a symlink to the
244     source rather than copying it."""
246     for url in download_paths(search_list, file_arg, url):
247         if not quiet:
248             print "Source %s..." % url,
250         scheme, path = splittype(url)
251         name = file_arg
253         if scheme in [None, 'file']:
254             if os.path.exists(path) is False:
255                 if not quiet:
256                     print "not found, skipping file copy"
257                 continue
258             elif name and name != path:
259                 if link_arg is False:
260                     if not quiet:
261                         print "\n    copying..."
262                     shutil.copy2(path, name)
263                 else:
264                     if not quiet:
265                         print "\n    linking..."
266                     os.symlink(path, name)
267         elif scheme in ['http', 'https', 'ftp']:
268             if not quiet:
269                 print "\n    downloading...",
270             name = download(url, file_arg, quiet)
271             if name is None:
272                 if not quiet:
273                     print "failed"
274                 continue
276         yield name
279 def usage():
280     print "Usage: %s [-a|--user-agent (user-agent)] [-f|--file (file)] [-l|--link] " \
281         "[-h|--hash (hash)] [-s|--search (search-dir)] [-S|--sigurl (signature-url)] " \
282         "--url (url)" % (sys.argv[0].split('/')[-1])
283     sys.exit(1)
286 def main():
287     import getopt
289     # FLUSH STDOUT
290     sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
292     user_agent_arg = None
293     file_arg = None
294     link_arg = False
295     keep_arg = False
296     hash_arg = None
297     url_arg = None
298     sig_arg = None
299     search_list = list()
301     try:
302         opts, args = getopt.getopt(sys.argv[1:], "a:f:h:lks:u:",
303                                    ["file=", "link", "keep", "hash=", "search=", "url=",
304                                     "sigurl=", "user-agent="])
305     except getopt.GetoptError, err:
306         print str(err)
307         usage()
309     for opt, arg in opts:
310         if opt in ["-a", "--user-agent"]:
311             user_agent_arg = arg
312         elif opt in ["-f", "--file"]:
313             file_arg = arg
314         elif opt in ["-l", "--link"]:
315             link_arg = True
316         elif opt in ["-k", "--keep"]:
317             keep_arg = True
318         elif opt in ["-h", "--hash"]:
319             hash_arg = arg
320         elif opt in ["-s", "--search"]:
321             search_list.append(arg)
322         elif opt in ["-S", "--sigurl"]:
323             sig_arg = arg
324         elif opt in ["-u", "--url"]:
325             url_arg = arg
326         else:
327             assert False, "unknown option"
329     if url_arg is None:
330         usage()
332     for name in download_from_paths(search_list, file_arg, url_arg, link_arg):
333         print "\n    validating signature...",
335         sig_valid = False
336         if not sig_arg:
337             print "skipping (no signature URL)"
338         else:
339             # Put the signature file in the same directory as the
340             # file we're downloading.
341             sig_file = os.path.join(
342                 os.path.dirname(file_arg),
343                 os.path.basename(sig_arg))
344             # Validate with the first signature we find.
345             for sig_file in download_from_paths(search_list, sig_file,
346                                                 sig_arg, link_arg, True):
347                 if sig_file:
348                     if validate_signature(name, sig_file):
349                         print "ok"
350                         sig_valid = True
351                     else:
352                         print "failed"
353                     break
354                 else:
355                     continue
356             else:
357                 print "failed (couldn't fetch signature)"
359         print "    validating hash...",
360         realhash = validate_container(name, hash_arg)
362         if not hash_arg:
363             print "skipping (no hash)"
364             print "hash is: %s" % realhash
365         elif realhash == hash_arg:
366             print "ok"
367         else:
368             payloadhash = validate_payload(name, hash_arg)
369             if payloadhash == hash_arg:
370                 print "ok"
371             else:
372                 # If the signature validated, then we assume
373                 # that the expected hash is just a typo, but we
374                 # warn just in case.
375                 if sig_valid:
376                     print "invalid hash!"
377                 else:
378                     print "corruption detected"
380                 print "    expected: %s" % hash_arg
381                 print "    actual:   %s" % realhash
382                 print "    payload:  %s" % payloadhash
384                 # An invalid hash shouldn't cause us to remove
385                 # the target file if the signature was valid.
386                 if not sig_valid:
387                     try:
388                         os.remove(name)
389                     except OSError:
390                         pass
392                     continue
394         sys.exit(0)
395     sys.exit(1)
398 if __name__ == "__main__":
399     main()