1 # Copyright (C) 2011, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import sys
, os
, socket
, ssl
6 from zeroinstall
import _
7 from zeroinstall
.injector
import download
8 from zeroinstall
.support
import ssl_match_hostname
10 if sys
.version_info
[0] > 2:
11 from urllib
import request
as urllib2
12 from http
.client
import HTTPSConnection
, HTTPException
15 from httplib
import HTTPSConnection
, HTTPException
18 # http://pypi.python.org/pypi/certifi
20 _fallback_ca_bundle
= certifi
.where()
22 # Final fallback (last known signer of keylookup)
23 _fallback_ca_bundle
= os
.path
.join(os
.path
.dirname(__file__
), "EquifaxSecureCA.crt")
25 # Note: on MacOS X at least, it will also look in the system keychain provided that you supply *some* CAs.
26 # (if you don't specify any trusted CAs, Python trusts everything!)
27 # So, the "fallback" option doesn't necessarily mean that other sites won't work.
29 "/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu/Arch Linux
30 "/etc/pki/tls/certs/ca-bundle.crt", # Fedora/RHEL
31 "/etc/ssl/ca-bundle.pem", # openSUSE/SLE (claimed)
32 "/var/lib/ca-certificates/ca-bundle.pem.new", # openSUSE (actual)
34 if os
.path
.exists(ca_bundle
):
35 class ValidatingHTTPSConnection(HTTPSConnection
):
37 sock
= socket
.create_connection((self
.host
, self
.port
), self
.timeout
)
38 if hasattr(self
, '_tunnel_host') and self
._tunnel
_host
:
41 sock
= ssl
.wrap_socket(sock
, cert_reqs
= ssl
.CERT_REQUIRED
, ca_certs
= ca_bundle
)
42 ssl_match_hostname
.match_hostname(sock
.getpeercert(), self
.host
)
45 class ValidatingHTTPSHandler(urllib2
.HTTPSHandler
):
46 def https_open(self
, req
):
47 return self
.do_open(self
.getConnection
, req
)
49 def getConnection(self
, host
, timeout
=300):
50 return ValidatingHTTPSConnection(host
)
51 MyHTTPSHandler
= ValidatingHTTPSHandler
54 raise Exception("No root CA's found (not even the built-in one!); security of HTTPS connections cannot be verified")
56 class Redirect(Exception):
57 def __init__(self
, req
):
58 Exception.__init
__(self
, "Redirect")
61 class MyRedirectHandler(urllib2
.HTTPRedirectHandler
):
62 """Throw an exception on redirects instead of continuing. The redirect will be handled in the main thread
63 so it can work with connection pooling."""
64 def redirect_request(self
, req
, fp
, code
, msg
, headers
, newurl
):
65 new_req
= urllib2
.HTTPRedirectHandler
.redirect_request(self
, req
, fp
, code
, msg
, headers
, newurl
)
67 raise Redirect(new_req
)
69 # Our handler differs from the Python default in that:
70 # - we don't support file:// URLs
71 # - we don't follow HTTP redirects
72 _my_urlopen
= urllib2
.OpenerDirector()
73 for klass
in [urllib2
.ProxyHandler
, urllib2
.UnknownHandler
, urllib2
.HTTPHandler
,
74 urllib2
.HTTPDefaultErrorHandler
, MyRedirectHandler
,
75 urllib2
.FTPHandler
, urllib2
.HTTPErrorProcessor
, MyHTTPSHandler
]:
76 _my_urlopen
.add_handler(klass())
78 def download_in_thread(url
, target_file
, if_modified_since
, notify_done
):
81 #print "Child downloading", url
82 if url
.startswith('http:') or url
.startswith('https:') or url
.startswith('ftp:'):
83 req
= urllib2
.Request(url
)
84 if url
.startswith('http:') and if_modified_since
:
85 req
.add_header('If-Modified-Since', if_modified_since
)
86 src
= _my_urlopen
.open(req
)
88 raise Exception(_('Unsupported URL protocol in: %s') % url
)
90 if sys
.version_info
[0] > 2:
91 sock_recv
= src
.fp
.read1
# Python 3
94 sock_recv
= src
.fp
._sock
.recv
# Python 2
95 except AttributeError:
96 sock_recv
= src
.fp
.fp
._sock
.recv
# Python 2.5 on FreeBSD
100 target_file
.write(data
)
103 notify_done(download
.RESULT_OK
)
104 except (urllib2
.HTTPError
, urllib2
.URLError
, HTTPException
, socket
.error
) as ex
:
105 if isinstance(ex
, urllib2
.HTTPError
) and ex
.code
== 304: # Not modified
106 notify_done(download
.RESULT_NOT_MODIFIED
)
108 #print >>sys.stderr, "Error downloading '" + url + "': " + (str(ex) or str(ex.__class__.__name__))
109 __
, ex
, tb
= sys
.exc_info()
110 notify_done(download
.RESULT_FAILED
, (download
.DownloadError(_('Error downloading {url}: {ex}').format(url
= url
, ex
= ex
)), tb
))
111 except Redirect
as ex
:
112 notify_done(download
.RESULT_REDIRECT
, redirect
= ex
.req
.get_full_url())
113 except Exception as ex
:
114 __
, ex
, tb
= sys
.exc_info()
115 notify_done(download
.RESULT_FAILED
, (ex
, tb
))