##// END OF EJS Templates
https: support tls sni (server name indication) for https urls (issue3090)...
Alex Orange -
r23834:bf07c19b default
parent child Browse files
Show More
@@ -1,186 +1,211 b''
1 1 # sslutil.py - SSL handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9 import os, sys
10 10
11 11 from mercurial import util
12 12 from mercurial.i18n import _
13 13 try:
14 14 # avoid using deprecated/broken FakeSocket in python 2.6
15 15 import ssl
16 16 CERT_REQUIRED = ssl.CERT_REQUIRED
17 17 PROTOCOL_TLSv1 = ssl.PROTOCOL_TLSv1
18 def ssl_wrap_socket(sock, keyfile, certfile, ssl_version=PROTOCOL_TLSv1,
19 cert_reqs=ssl.CERT_NONE, ca_certs=None):
20 sslsocket = ssl.wrap_socket(sock, keyfile, certfile,
21 cert_reqs=cert_reqs, ca_certs=ca_certs,
22 ssl_version=ssl_version)
23 # check if wrap_socket failed silently because socket had been closed
24 # - see http://bugs.python.org/issue13721
25 if not sslsocket.cipher():
26 raise util.Abort(_('ssl connection failed'))
27 return sslsocket
18 try:
19 ssl_context = ssl.SSLContext
20
21 def ssl_wrap_socket(sock, keyfile, certfile, ssl_version=PROTOCOL_TLSv1,
22 cert_reqs=ssl.CERT_NONE, ca_certs=None,
23 serverhostname=None):
24 sslcontext = ssl.SSLContext(ssl_version)
25 if certfile is not None:
26 sslcontext.load_cert_chain(certfile, keyfile)
27 sslcontext.verify_mode = cert_reqs
28 if ca_certs is not None:
29 sslcontext.load_verify_locations(cafile=ca_certs)
30
31 sslsocket = sslcontext.wrap_socket(sock,
32 server_hostname=serverhostname)
33 # check if wrap_socket failed silently because socket had been
34 # closed
35 # - see http://bugs.python.org/issue13721
36 if not sslsocket.cipher():
37 raise util.Abort(_('ssl connection failed'))
38 return sslsocket
39 except AttributeError:
40 def ssl_wrap_socket(sock, keyfile, certfile, ssl_version=PROTOCOL_TLSv1,
41 cert_reqs=ssl.CERT_NONE, ca_certs=None,
42 serverhostname=None):
43 sslsocket = ssl.wrap_socket(sock, keyfile, certfile,
44 cert_reqs=cert_reqs, ca_certs=ca_certs,
45 ssl_version=ssl_version)
46 # check if wrap_socket failed silently because socket had been
47 # closed
48 # - see http://bugs.python.org/issue13721
49 if not sslsocket.cipher():
50 raise util.Abort(_('ssl connection failed'))
51 return sslsocket
28 52 except ImportError:
29 53 CERT_REQUIRED = 2
30 54
31 55 PROTOCOL_TLSv1 = 3
32 56
33 57 import socket, httplib
34 58
35 59 def ssl_wrap_socket(sock, keyfile, certfile, ssl_version=PROTOCOL_TLSv1,
36 cert_reqs=CERT_REQUIRED, ca_certs=None):
60 cert_reqs=CERT_REQUIRED, ca_certs=None,
61 serverhostname=None):
37 62 if not util.safehasattr(socket, 'ssl'):
38 63 raise util.Abort(_('Python SSL support not found'))
39 64 if ca_certs:
40 65 raise util.Abort(_(
41 66 'certificate checking requires Python 2.6'))
42 67
43 68 ssl = socket.ssl(sock, keyfile, certfile)
44 69 return httplib.FakeSocket(sock, ssl)
45 70
46 71 def _verifycert(cert, hostname):
47 72 '''Verify that cert (in socket.getpeercert() format) matches hostname.
48 73 CRLs is not handled.
49 74
50 75 Returns error message if any problems are found and None on success.
51 76 '''
52 77 if not cert:
53 78 return _('no certificate received')
54 79 dnsname = hostname.lower()
55 80 def matchdnsname(certname):
56 81 return (certname == dnsname or
57 82 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
58 83
59 84 san = cert.get('subjectAltName', [])
60 85 if san:
61 86 certnames = [value.lower() for key, value in san if key == 'DNS']
62 87 for name in certnames:
63 88 if matchdnsname(name):
64 89 return None
65 90 if certnames:
66 91 return _('certificate is for %s') % ', '.join(certnames)
67 92
68 93 # subject is only checked when subjectAltName is empty
69 94 for s in cert.get('subject', []):
70 95 key, value = s[0]
71 96 if key == 'commonName':
72 97 try:
73 98 # 'subject' entries are unicode
74 99 certname = value.lower().encode('ascii')
75 100 except UnicodeEncodeError:
76 101 return _('IDN in certificate not supported')
77 102 if matchdnsname(certname):
78 103 return None
79 104 return _('certificate is for %s') % certname
80 105 return _('no commonName or subjectAltName found in certificate')
81 106
82 107
83 108 # CERT_REQUIRED means fetch the cert from the server all the time AND
84 109 # validate it against the CA store provided in web.cacerts.
85 110 #
86 111 # We COMPLETELY ignore CERT_REQUIRED on Python <= 2.5, as it's totally
87 112 # busted on those versions.
88 113
89 114 def _plainapplepython():
90 115 """return true if this seems to be a pure Apple Python that
91 116 * is unfrozen and presumably has the whole mercurial module in the file
92 117 system
93 118 * presumably is an Apple Python that uses Apple OpenSSL which has patches
94 119 for using system certificate store CAs in addition to the provided
95 120 cacerts file
96 121 """
97 122 if sys.platform != 'darwin' or util.mainfrozen():
98 123 return False
99 124 exe = (sys.executable or '').lower()
100 125 return (exe.startswith('/usr/bin/python') or
101 126 exe.startswith('/system/library/frameworks/python.framework/'))
102 127
103 128 def sslkwargs(ui, host):
104 129 kws = {'ssl_version': PROTOCOL_TLSv1,
105 130 }
106 131 hostfingerprint = ui.config('hostfingerprints', host)
107 132 if hostfingerprint:
108 133 return kws
109 134 cacerts = ui.config('web', 'cacerts')
110 135 if cacerts:
111 136 cacerts = util.expandpath(cacerts)
112 137 if not os.path.exists(cacerts):
113 138 raise util.Abort(_('could not find web.cacerts: %s') % cacerts)
114 139 elif cacerts is None and _plainapplepython():
115 140 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
116 141 if os.path.exists(dummycert):
117 142 ui.debug('using %s to enable OS X system CA\n' % dummycert)
118 143 ui.setconfig('web', 'cacerts', dummycert, 'dummy')
119 144 cacerts = dummycert
120 145 if cacerts:
121 146 kws.update({'ca_certs': cacerts,
122 147 'cert_reqs': CERT_REQUIRED,
123 148 })
124 149 return kws
125 150
126 151 class validator(object):
127 152 def __init__(self, ui, host):
128 153 self.ui = ui
129 154 self.host = host
130 155
131 156 def __call__(self, sock, strict=False):
132 157 host = self.host
133 158 cacerts = self.ui.config('web', 'cacerts')
134 159 hostfingerprint = self.ui.config('hostfingerprints', host)
135 160 if not getattr(sock, 'getpeercert', False): # python 2.5 ?
136 161 if hostfingerprint:
137 162 raise util.Abort(_("host fingerprint for %s can't be "
138 163 "verified (Python too old)") % host)
139 164 if strict:
140 165 raise util.Abort(_("certificate for %s can't be verified "
141 166 "(Python too old)") % host)
142 167 if self.ui.configbool('ui', 'reportoldssl', True):
143 168 self.ui.warn(_("warning: certificate for %s can't be verified "
144 169 "(Python too old)\n") % host)
145 170 return
146 171
147 172 if not sock.cipher(): # work around http://bugs.python.org/issue13721
148 173 raise util.Abort(_('%s ssl connection error') % host)
149 174 try:
150 175 peercert = sock.getpeercert(True)
151 176 peercert2 = sock.getpeercert()
152 177 except AttributeError:
153 178 raise util.Abort(_('%s ssl connection error') % host)
154 179
155 180 if not peercert:
156 181 raise util.Abort(_('%s certificate error: '
157 182 'no certificate received') % host)
158 183 peerfingerprint = util.sha1(peercert).hexdigest()
159 184 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
160 185 for x in xrange(0, len(peerfingerprint), 2)])
161 186 if hostfingerprint:
162 187 if peerfingerprint.lower() != \
163 188 hostfingerprint.replace(':', '').lower():
164 189 raise util.Abort(_('certificate for %s has unexpected '
165 190 'fingerprint %s') % (host, nicefingerprint),
166 191 hint=_('check hostfingerprint configuration'))
167 192 self.ui.debug('%s certificate matched fingerprint %s\n' %
168 193 (host, nicefingerprint))
169 194 elif cacerts:
170 195 msg = _verifycert(peercert2, host)
171 196 if msg:
172 197 raise util.Abort(_('%s certificate error: %s') % (host, msg),
173 198 hint=_('configure hostfingerprint %s or use '
174 199 '--insecure to connect insecurely') %
175 200 nicefingerprint)
176 201 self.ui.debug('%s certificate successfully verified\n' % host)
177 202 elif strict:
178 203 raise util.Abort(_('%s certificate with fingerprint %s not '
179 204 'verified') % (host, nicefingerprint),
180 205 hint=_('check hostfingerprints or web.cacerts '
181 206 'config setting'))
182 207 else:
183 208 self.ui.warn(_('warning: %s certificate with fingerprint %s not '
184 209 'verified (check hostfingerprints or web.cacerts '
185 210 'config setting)\n') %
186 211 (host, nicefingerprint))
@@ -1,509 +1,510 b''
1 1 # url.py - HTTP handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 import urllib, urllib2, httplib, os, socket, cStringIO, base64
11 11 from i18n import _
12 12 import keepalive, util, sslutil
13 13 import httpconnection as httpconnectionmod
14 14
15 15 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
16 16 def __init__(self, ui):
17 17 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
18 18 self.ui = ui
19 19
20 20 def find_user_password(self, realm, authuri):
21 21 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
22 22 self, realm, authuri)
23 23 user, passwd = authinfo
24 24 if user and passwd:
25 25 self._writedebug(user, passwd)
26 26 return (user, passwd)
27 27
28 28 if not user or not passwd:
29 29 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
30 30 if res:
31 31 group, auth = res
32 32 user, passwd = auth.get('username'), auth.get('password')
33 33 self.ui.debug("using auth.%s.* for authentication\n" % group)
34 34 if not user or not passwd:
35 35 u = util.url(authuri)
36 36 u.query = None
37 37 if not self.ui.interactive():
38 38 raise util.Abort(_('http authorization required for %s') %
39 39 util.hidepassword(str(u)))
40 40
41 41 self.ui.write(_("http authorization required for %s\n") %
42 42 util.hidepassword(str(u)))
43 43 self.ui.write(_("realm: %s\n") % realm)
44 44 if user:
45 45 self.ui.write(_("user: %s\n") % user)
46 46 else:
47 47 user = self.ui.prompt(_("user:"), default=None)
48 48
49 49 if not passwd:
50 50 passwd = self.ui.getpass()
51 51
52 52 self.add_password(realm, authuri, user, passwd)
53 53 self._writedebug(user, passwd)
54 54 return (user, passwd)
55 55
56 56 def _writedebug(self, user, passwd):
57 57 msg = _('http auth: user %s, password %s\n')
58 58 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
59 59
60 60 def find_stored_password(self, authuri):
61 61 return urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
62 62 self, None, authuri)
63 63
64 64 class proxyhandler(urllib2.ProxyHandler):
65 65 def __init__(self, ui):
66 66 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
67 67 # XXX proxyauthinfo = None
68 68
69 69 if proxyurl:
70 70 # proxy can be proper url or host[:port]
71 71 if not (proxyurl.startswith('http:') or
72 72 proxyurl.startswith('https:')):
73 73 proxyurl = 'http://' + proxyurl + '/'
74 74 proxy = util.url(proxyurl)
75 75 if not proxy.user:
76 76 proxy.user = ui.config("http_proxy", "user")
77 77 proxy.passwd = ui.config("http_proxy", "passwd")
78 78
79 79 # see if we should use a proxy for this url
80 80 no_list = ["localhost", "127.0.0.1"]
81 81 no_list.extend([p.lower() for
82 82 p in ui.configlist("http_proxy", "no")])
83 83 no_list.extend([p.strip().lower() for
84 84 p in os.getenv("no_proxy", '').split(',')
85 85 if p.strip()])
86 86 # "http_proxy.always" config is for running tests on localhost
87 87 if ui.configbool("http_proxy", "always"):
88 88 self.no_list = []
89 89 else:
90 90 self.no_list = no_list
91 91
92 92 proxyurl = str(proxy)
93 93 proxies = {'http': proxyurl, 'https': proxyurl}
94 94 ui.debug('proxying through http://%s:%s\n' %
95 95 (proxy.host, proxy.port))
96 96 else:
97 97 proxies = {}
98 98
99 99 # urllib2 takes proxy values from the environment and those
100 100 # will take precedence if found. So, if there's a config entry
101 101 # defining a proxy, drop the environment ones
102 102 if ui.config("http_proxy", "host"):
103 103 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
104 104 try:
105 105 if env in os.environ:
106 106 del os.environ[env]
107 107 except OSError:
108 108 pass
109 109
110 110 urllib2.ProxyHandler.__init__(self, proxies)
111 111 self.ui = ui
112 112
113 113 def proxy_open(self, req, proxy, type_):
114 114 host = req.get_host().split(':')[0]
115 115 for e in self.no_list:
116 116 if host == e:
117 117 return None
118 118 if e.startswith('*.') and host.endswith(e[2:]):
119 119 return None
120 120 if e.startswith('.') and host.endswith(e[1:]):
121 121 return None
122 122
123 123 # work around a bug in Python < 2.4.2
124 124 # (it leaves a "\n" at the end of Proxy-authorization headers)
125 125 baseclass = req.__class__
126 126 class _request(baseclass):
127 127 def add_header(self, key, val):
128 128 if key.lower() == 'proxy-authorization':
129 129 val = val.strip()
130 130 return baseclass.add_header(self, key, val)
131 131 req.__class__ = _request
132 132
133 133 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type_)
134 134
135 135 def _gen_sendfile(orgsend):
136 136 def _sendfile(self, data):
137 137 # send a file
138 138 if isinstance(data, httpconnectionmod.httpsendfile):
139 139 # if auth required, some data sent twice, so rewind here
140 140 data.seek(0)
141 141 for chunk in util.filechunkiter(data):
142 142 orgsend(self, chunk)
143 143 else:
144 144 orgsend(self, data)
145 145 return _sendfile
146 146
147 147 has_https = util.safehasattr(urllib2, 'HTTPSHandler')
148 148 if has_https:
149 149 try:
150 150 _create_connection = socket.create_connection
151 151 except AttributeError:
152 152 _GLOBAL_DEFAULT_TIMEOUT = object()
153 153
154 154 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
155 155 source_address=None):
156 156 # lifted from Python 2.6
157 157
158 158 msg = "getaddrinfo returns an empty list"
159 159 host, port = address
160 160 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
161 161 af, socktype, proto, canonname, sa = res
162 162 sock = None
163 163 try:
164 164 sock = socket.socket(af, socktype, proto)
165 165 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
166 166 sock.settimeout(timeout)
167 167 if source_address:
168 168 sock.bind(source_address)
169 169 sock.connect(sa)
170 170 return sock
171 171
172 172 except socket.error, msg:
173 173 if sock is not None:
174 174 sock.close()
175 175
176 176 raise socket.error(msg)
177 177
178 178 class httpconnection(keepalive.HTTPConnection):
179 179 # must be able to send big bundle as stream.
180 180 send = _gen_sendfile(keepalive.HTTPConnection.send)
181 181
182 182 def connect(self):
183 183 if has_https and self.realhostport: # use CONNECT proxy
184 184 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
185 185 self.sock.connect((self.host, self.port))
186 186 if _generic_proxytunnel(self):
187 187 # we do not support client X.509 certificates
188 self.sock = sslutil.ssl_wrap_socket(self.sock, None, None)
188 self.sock = sslutil.ssl_wrap_socket(self.sock, None, None,
189 serverhostname=self.host)
189 190 else:
190 191 keepalive.HTTPConnection.connect(self)
191 192
192 193 def getresponse(self):
193 194 proxyres = getattr(self, 'proxyres', None)
194 195 if proxyres:
195 196 if proxyres.will_close:
196 197 self.close()
197 198 self.proxyres = None
198 199 return proxyres
199 200 return keepalive.HTTPConnection.getresponse(self)
200 201
201 202 # general transaction handler to support different ways to handle
202 203 # HTTPS proxying before and after Python 2.6.3.
203 204 def _generic_start_transaction(handler, h, req):
204 205 tunnel_host = getattr(req, '_tunnel_host', None)
205 206 if tunnel_host:
206 207 if tunnel_host[:7] not in ['http://', 'https:/']:
207 208 tunnel_host = 'https://' + tunnel_host
208 209 new_tunnel = True
209 210 else:
210 211 tunnel_host = req.get_selector()
211 212 new_tunnel = False
212 213
213 214 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
214 215 u = util.url(tunnel_host)
215 216 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
216 217 h.realhostport = ':'.join([u.host, (u.port or '443')])
217 218 h.headers = req.headers.copy()
218 219 h.headers.update(handler.parent.addheaders)
219 220 return
220 221
221 222 h.realhostport = None
222 223 h.headers = None
223 224
224 225 def _generic_proxytunnel(self):
225 226 proxyheaders = dict(
226 227 [(x, self.headers[x]) for x in self.headers
227 228 if x.lower().startswith('proxy-')])
228 229 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
229 230 for header in proxyheaders.iteritems():
230 231 self.send('%s: %s\r\n' % header)
231 232 self.send('\r\n')
232 233
233 234 # majority of the following code is duplicated from
234 235 # httplib.HTTPConnection as there are no adequate places to
235 236 # override functions to provide the needed functionality
236 237 res = self.response_class(self.sock,
237 238 strict=self.strict,
238 239 method=self._method)
239 240
240 241 while True:
241 242 version, status, reason = res._read_status()
242 243 if status != httplib.CONTINUE:
243 244 break
244 245 while True:
245 246 skip = res.fp.readline().strip()
246 247 if not skip:
247 248 break
248 249 res.status = status
249 250 res.reason = reason.strip()
250 251
251 252 if res.status == 200:
252 253 while True:
253 254 line = res.fp.readline()
254 255 if line == '\r\n':
255 256 break
256 257 return True
257 258
258 259 if version == 'HTTP/1.0':
259 260 res.version = 10
260 261 elif version.startswith('HTTP/1.'):
261 262 res.version = 11
262 263 elif version == 'HTTP/0.9':
263 264 res.version = 9
264 265 else:
265 266 raise httplib.UnknownProtocol(version)
266 267
267 268 if res.version == 9:
268 269 res.length = None
269 270 res.chunked = 0
270 271 res.will_close = 1
271 272 res.msg = httplib.HTTPMessage(cStringIO.StringIO())
272 273 return False
273 274
274 275 res.msg = httplib.HTTPMessage(res.fp)
275 276 res.msg.fp = None
276 277
277 278 # are we using the chunked-style of transfer encoding?
278 279 trenc = res.msg.getheader('transfer-encoding')
279 280 if trenc and trenc.lower() == "chunked":
280 281 res.chunked = 1
281 282 res.chunk_left = None
282 283 else:
283 284 res.chunked = 0
284 285
285 286 # will the connection close at the end of the response?
286 287 res.will_close = res._check_close()
287 288
288 289 # do we have a Content-Length?
289 290 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
290 291 # transfer-encoding is "chunked"
291 292 length = res.msg.getheader('content-length')
292 293 if length and not res.chunked:
293 294 try:
294 295 res.length = int(length)
295 296 except ValueError:
296 297 res.length = None
297 298 else:
298 299 if res.length < 0: # ignore nonsensical negative lengths
299 300 res.length = None
300 301 else:
301 302 res.length = None
302 303
303 304 # does the body have a fixed length? (of zero)
304 305 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
305 306 100 <= status < 200 or # 1xx codes
306 307 res._method == 'HEAD'):
307 308 res.length = 0
308 309
309 310 # if the connection remains open, and we aren't using chunked, and
310 311 # a content-length was not provided, then assume that the connection
311 312 # WILL close.
312 313 if (not res.will_close and
313 314 not res.chunked and
314 315 res.length is None):
315 316 res.will_close = 1
316 317
317 318 self.proxyres = res
318 319
319 320 return False
320 321
321 322 class httphandler(keepalive.HTTPHandler):
322 323 def http_open(self, req):
323 324 return self.do_open(httpconnection, req)
324 325
325 326 def _start_transaction(self, h, req):
326 327 _generic_start_transaction(self, h, req)
327 328 return keepalive.HTTPHandler._start_transaction(self, h, req)
328 329
329 330 if has_https:
330 331 class httpsconnection(httplib.HTTPSConnection):
331 332 response_class = keepalive.HTTPResponse
332 333 # must be able to send big bundle as stream.
333 334 send = _gen_sendfile(keepalive.safesend)
334 335 getresponse = keepalive.wrapgetresponse(httplib.HTTPSConnection)
335 336
336 337 def connect(self):
337 338 self.sock = _create_connection((self.host, self.port))
338 339
339 340 host = self.host
340 341 if self.realhostport: # use CONNECT proxy
341 342 _generic_proxytunnel(self)
342 343 host = self.realhostport.rsplit(':', 1)[0]
343 344 self.sock = sslutil.ssl_wrap_socket(
344 self.sock, self.key_file, self.cert_file,
345 self.sock, self.key_file, self.cert_file, serverhostname=host,
345 346 **sslutil.sslkwargs(self.ui, host))
346 347 sslutil.validator(self.ui, host)(self.sock)
347 348
348 349 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler):
349 350 def __init__(self, ui):
350 351 keepalive.KeepAliveHandler.__init__(self)
351 352 urllib2.HTTPSHandler.__init__(self)
352 353 self.ui = ui
353 354 self.pwmgr = passwordmgr(self.ui)
354 355
355 356 def _start_transaction(self, h, req):
356 357 _generic_start_transaction(self, h, req)
357 358 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
358 359
359 360 def https_open(self, req):
360 361 # req.get_full_url() does not contain credentials and we may
361 362 # need them to match the certificates.
362 363 url = req.get_full_url()
363 364 user, password = self.pwmgr.find_stored_password(url)
364 365 res = httpconnectionmod.readauthforuri(self.ui, url, user)
365 366 if res:
366 367 group, auth = res
367 368 self.auth = auth
368 369 self.ui.debug("using auth.%s.* for authentication\n" % group)
369 370 else:
370 371 self.auth = None
371 372 return self.do_open(self._makeconnection, req)
372 373
373 374 def _makeconnection(self, host, port=None, *args, **kwargs):
374 375 keyfile = None
375 376 certfile = None
376 377
377 378 if len(args) >= 1: # key_file
378 379 keyfile = args[0]
379 380 if len(args) >= 2: # cert_file
380 381 certfile = args[1]
381 382 args = args[2:]
382 383
383 384 # if the user has specified different key/cert files in
384 385 # hgrc, we prefer these
385 386 if self.auth and 'key' in self.auth and 'cert' in self.auth:
386 387 keyfile = self.auth['key']
387 388 certfile = self.auth['cert']
388 389
389 390 conn = httpsconnection(host, port, keyfile, certfile, *args,
390 391 **kwargs)
391 392 conn.ui = self.ui
392 393 return conn
393 394
394 395 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
395 396 def __init__(self, *args, **kwargs):
396 397 urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs)
397 398 self.retried_req = None
398 399
399 400 def reset_retry_count(self):
400 401 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
401 402 # forever. We disable reset_retry_count completely and reset in
402 403 # http_error_auth_reqed instead.
403 404 pass
404 405
405 406 def http_error_auth_reqed(self, auth_header, host, req, headers):
406 407 # Reset the retry counter once for each request.
407 408 if req is not self.retried_req:
408 409 self.retried_req = req
409 410 self.retried = 0
410 411 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
411 412 # it doesn't know about the auth type requested. This can happen if
412 413 # somebody is using BasicAuth and types a bad password.
413 414 try:
414 415 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
415 416 self, auth_header, host, req, headers)
416 417 except ValueError, inst:
417 418 arg = inst.args[0]
418 419 if arg.startswith("AbstractDigestAuthHandler doesn't know "):
419 420 return
420 421 raise
421 422
422 423 class httpbasicauthhandler(urllib2.HTTPBasicAuthHandler):
423 424 def __init__(self, *args, **kwargs):
424 425 self.auth = None
425 426 urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
426 427 self.retried_req = None
427 428
428 429 def http_request(self, request):
429 430 if self.auth:
430 431 request.add_unredirected_header(self.auth_header, self.auth)
431 432
432 433 return request
433 434
434 435 def https_request(self, request):
435 436 if self.auth:
436 437 request.add_unredirected_header(self.auth_header, self.auth)
437 438
438 439 return request
439 440
440 441 def reset_retry_count(self):
441 442 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
442 443 # forever. We disable reset_retry_count completely and reset in
443 444 # http_error_auth_reqed instead.
444 445 pass
445 446
446 447 def http_error_auth_reqed(self, auth_header, host, req, headers):
447 448 # Reset the retry counter once for each request.
448 449 if req is not self.retried_req:
449 450 self.retried_req = req
450 451 self.retried = 0
451 452 return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed(
452 453 self, auth_header, host, req, headers)
453 454
454 455 def retry_http_basic_auth(self, host, req, realm):
455 456 user, pw = self.passwd.find_user_password(realm, req.get_full_url())
456 457 if pw is not None:
457 458 raw = "%s:%s" % (user, pw)
458 459 auth = 'Basic %s' % base64.b64encode(raw).strip()
459 460 if req.headers.get(self.auth_header, None) == auth:
460 461 return None
461 462 self.auth = auth
462 463 req.add_unredirected_header(self.auth_header, auth)
463 464 return self.parent.open(req)
464 465 else:
465 466 return None
466 467
467 468 handlerfuncs = []
468 469
469 470 def opener(ui, authinfo=None):
470 471 '''
471 472 construct an opener suitable for urllib2
472 473 authinfo will be added to the password manager
473 474 '''
474 475 if ui.configbool('ui', 'usehttp2', False):
475 476 handlers = [httpconnectionmod.http2handler(ui, passwordmgr(ui))]
476 477 else:
477 478 handlers = [httphandler()]
478 479 if has_https:
479 480 handlers.append(httpshandler(ui))
480 481
481 482 handlers.append(proxyhandler(ui))
482 483
483 484 passmgr = passwordmgr(ui)
484 485 if authinfo is not None:
485 486 passmgr.add_password(*authinfo)
486 487 user, passwd = authinfo[2:4]
487 488 ui.debug('http auth: user %s, password %s\n' %
488 489 (user, passwd and '*' * len(passwd) or 'not set'))
489 490
490 491 handlers.extend((httpbasicauthhandler(passmgr),
491 492 httpdigestauthhandler(passmgr)))
492 493 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
493 494 opener = urllib2.build_opener(*handlers)
494 495
495 496 # 1.0 here is the _protocol_ version
496 497 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
497 498 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
498 499 return opener
499 500
500 501 def open(ui, url_, data=None):
501 502 u = util.url(url_)
502 503 if u.scheme:
503 504 u.scheme = u.scheme.lower()
504 505 url_, authinfo = u.authinfo()
505 506 else:
506 507 path = util.normpath(os.path.abspath(url_))
507 508 url_ = 'file://' + urllib.pathname2url(path)
508 509 authinfo = None
509 510 return opener(ui, authinfo).open(url_, data)
General Comments 0
You need to be logged in to leave comments. Login now