##// END OF EJS Templates
sslutil: convert socket validation from a class to a function (API)...
Gregory Szorc -
r29227:dffe78d8 default
parent child Browse files
Show More
@@ -1,289 +1,289 b''
1 # httpconnection.py - urllib2 handler for new http support
1 # httpconnection.py - urllib2 handler for new http support
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 # Copyright 2011 Google, Inc.
6 # Copyright 2011 Google, Inc.
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 import logging
13 import logging
14 import os
14 import os
15 import socket
15 import socket
16
16
17 from .i18n import _
17 from .i18n import _
18 from . import (
18 from . import (
19 httpclient,
19 httpclient,
20 sslutil,
20 sslutil,
21 util,
21 util,
22 )
22 )
23
23
24 urlerr = util.urlerr
24 urlerr = util.urlerr
25 urlreq = util.urlreq
25 urlreq = util.urlreq
26
26
27 # moved here from url.py to avoid a cycle
27 # moved here from url.py to avoid a cycle
28 class httpsendfile(object):
28 class httpsendfile(object):
29 """This is a wrapper around the objects returned by python's "open".
29 """This is a wrapper around the objects returned by python's "open".
30
30
31 Its purpose is to send file-like objects via HTTP.
31 Its purpose is to send file-like objects via HTTP.
32 It do however not define a __len__ attribute because the length
32 It do however not define a __len__ attribute because the length
33 might be more than Py_ssize_t can handle.
33 might be more than Py_ssize_t can handle.
34 """
34 """
35
35
36 def __init__(self, ui, *args, **kwargs):
36 def __init__(self, ui, *args, **kwargs):
37 self.ui = ui
37 self.ui = ui
38 self._data = open(*args, **kwargs)
38 self._data = open(*args, **kwargs)
39 self.seek = self._data.seek
39 self.seek = self._data.seek
40 self.close = self._data.close
40 self.close = self._data.close
41 self.write = self._data.write
41 self.write = self._data.write
42 self.length = os.fstat(self._data.fileno()).st_size
42 self.length = os.fstat(self._data.fileno()).st_size
43 self._pos = 0
43 self._pos = 0
44 self._total = self.length // 1024 * 2
44 self._total = self.length // 1024 * 2
45
45
46 def read(self, *args, **kwargs):
46 def read(self, *args, **kwargs):
47 try:
47 try:
48 ret = self._data.read(*args, **kwargs)
48 ret = self._data.read(*args, **kwargs)
49 except EOFError:
49 except EOFError:
50 self.ui.progress(_('sending'), None)
50 self.ui.progress(_('sending'), None)
51 self._pos += len(ret)
51 self._pos += len(ret)
52 # We pass double the max for total because we currently have
52 # We pass double the max for total because we currently have
53 # to send the bundle twice in the case of a server that
53 # to send the bundle twice in the case of a server that
54 # requires authentication. Since we can't know until we try
54 # requires authentication. Since we can't know until we try
55 # once whether authentication will be required, just lie to
55 # once whether authentication will be required, just lie to
56 # the user and maybe the push succeeds suddenly at 50%.
56 # the user and maybe the push succeeds suddenly at 50%.
57 self.ui.progress(_('sending'), self._pos // 1024,
57 self.ui.progress(_('sending'), self._pos // 1024,
58 unit=_('kb'), total=self._total)
58 unit=_('kb'), total=self._total)
59 return ret
59 return ret
60
60
61 # moved here from url.py to avoid a cycle
61 # moved here from url.py to avoid a cycle
62 def readauthforuri(ui, uri, user):
62 def readauthforuri(ui, uri, user):
63 # Read configuration
63 # Read configuration
64 config = dict()
64 config = dict()
65 for key, val in ui.configitems('auth'):
65 for key, val in ui.configitems('auth'):
66 if '.' not in key:
66 if '.' not in key:
67 ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
67 ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
68 continue
68 continue
69 group, setting = key.rsplit('.', 1)
69 group, setting = key.rsplit('.', 1)
70 gdict = config.setdefault(group, dict())
70 gdict = config.setdefault(group, dict())
71 if setting in ('username', 'cert', 'key'):
71 if setting in ('username', 'cert', 'key'):
72 val = util.expandpath(val)
72 val = util.expandpath(val)
73 gdict[setting] = val
73 gdict[setting] = val
74
74
75 # Find the best match
75 # Find the best match
76 scheme, hostpath = uri.split('://', 1)
76 scheme, hostpath = uri.split('://', 1)
77 bestuser = None
77 bestuser = None
78 bestlen = 0
78 bestlen = 0
79 bestauth = None
79 bestauth = None
80 for group, auth in config.iteritems():
80 for group, auth in config.iteritems():
81 if user and user != auth.get('username', user):
81 if user and user != auth.get('username', user):
82 # If a username was set in the URI, the entry username
82 # If a username was set in the URI, the entry username
83 # must either match it or be unset
83 # must either match it or be unset
84 continue
84 continue
85 prefix = auth.get('prefix')
85 prefix = auth.get('prefix')
86 if not prefix:
86 if not prefix:
87 continue
87 continue
88 p = prefix.split('://', 1)
88 p = prefix.split('://', 1)
89 if len(p) > 1:
89 if len(p) > 1:
90 schemes, prefix = [p[0]], p[1]
90 schemes, prefix = [p[0]], p[1]
91 else:
91 else:
92 schemes = (auth.get('schemes') or 'https').split()
92 schemes = (auth.get('schemes') or 'https').split()
93 if (prefix == '*' or hostpath.startswith(prefix)) and \
93 if (prefix == '*' or hostpath.startswith(prefix)) and \
94 (len(prefix) > bestlen or (len(prefix) == bestlen and \
94 (len(prefix) > bestlen or (len(prefix) == bestlen and \
95 not bestuser and 'username' in auth)) \
95 not bestuser and 'username' in auth)) \
96 and scheme in schemes:
96 and scheme in schemes:
97 bestlen = len(prefix)
97 bestlen = len(prefix)
98 bestauth = group, auth
98 bestauth = group, auth
99 bestuser = auth.get('username')
99 bestuser = auth.get('username')
100 if user and not bestuser:
100 if user and not bestuser:
101 auth['username'] = user
101 auth['username'] = user
102 return bestauth
102 return bestauth
103
103
104 # Mercurial (at least until we can remove the old codepath) requires
104 # Mercurial (at least until we can remove the old codepath) requires
105 # that the http response object be sufficiently file-like, so we
105 # that the http response object be sufficiently file-like, so we
106 # provide a close() method here.
106 # provide a close() method here.
107 class HTTPResponse(httpclient.HTTPResponse):
107 class HTTPResponse(httpclient.HTTPResponse):
108 def close(self):
108 def close(self):
109 pass
109 pass
110
110
111 class HTTPConnection(httpclient.HTTPConnection):
111 class HTTPConnection(httpclient.HTTPConnection):
112 response_class = HTTPResponse
112 response_class = HTTPResponse
113 def request(self, method, uri, body=None, headers=None):
113 def request(self, method, uri, body=None, headers=None):
114 if headers is None:
114 if headers is None:
115 headers = {}
115 headers = {}
116 if isinstance(body, httpsendfile):
116 if isinstance(body, httpsendfile):
117 body.seek(0)
117 body.seek(0)
118 httpclient.HTTPConnection.request(self, method, uri, body=body,
118 httpclient.HTTPConnection.request(self, method, uri, body=body,
119 headers=headers)
119 headers=headers)
120
120
121
121
122 _configuredlogging = False
122 _configuredlogging = False
123 LOGFMT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
123 LOGFMT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
124 # Subclass BOTH of these because otherwise urllib2 "helpfully"
124 # Subclass BOTH of these because otherwise urllib2 "helpfully"
125 # reinserts them since it notices we don't include any subclasses of
125 # reinserts them since it notices we don't include any subclasses of
126 # them.
126 # them.
127 class http2handler(urlreq.httphandler, urlreq.httpshandler):
127 class http2handler(urlreq.httphandler, urlreq.httpshandler):
128 def __init__(self, ui, pwmgr):
128 def __init__(self, ui, pwmgr):
129 global _configuredlogging
129 global _configuredlogging
130 urlreq.abstracthttphandler.__init__(self)
130 urlreq.abstracthttphandler.__init__(self)
131 self.ui = ui
131 self.ui = ui
132 self.pwmgr = pwmgr
132 self.pwmgr = pwmgr
133 self._connections = {}
133 self._connections = {}
134 # developer config: ui.http2debuglevel
134 # developer config: ui.http2debuglevel
135 loglevel = ui.config('ui', 'http2debuglevel', default=None)
135 loglevel = ui.config('ui', 'http2debuglevel', default=None)
136 if loglevel and not _configuredlogging:
136 if loglevel and not _configuredlogging:
137 _configuredlogging = True
137 _configuredlogging = True
138 logger = logging.getLogger('mercurial.httpclient')
138 logger = logging.getLogger('mercurial.httpclient')
139 logger.setLevel(getattr(logging, loglevel.upper()))
139 logger.setLevel(getattr(logging, loglevel.upper()))
140 handler = logging.StreamHandler()
140 handler = logging.StreamHandler()
141 handler.setFormatter(logging.Formatter(LOGFMT))
141 handler.setFormatter(logging.Formatter(LOGFMT))
142 logger.addHandler(handler)
142 logger.addHandler(handler)
143
143
144 def close_all(self):
144 def close_all(self):
145 """Close and remove all connection objects being kept for reuse."""
145 """Close and remove all connection objects being kept for reuse."""
146 for openconns in self._connections.values():
146 for openconns in self._connections.values():
147 for conn in openconns:
147 for conn in openconns:
148 conn.close()
148 conn.close()
149 self._connections = {}
149 self._connections = {}
150
150
151 # shamelessly borrowed from urllib2.AbstractHTTPHandler
151 # shamelessly borrowed from urllib2.AbstractHTTPHandler
152 def do_open(self, http_class, req, use_ssl):
152 def do_open(self, http_class, req, use_ssl):
153 """Return an addinfourl object for the request, using http_class.
153 """Return an addinfourl object for the request, using http_class.
154
154
155 http_class must implement the HTTPConnection API from httplib.
155 http_class must implement the HTTPConnection API from httplib.
156 The addinfourl return value is a file-like object. It also
156 The addinfourl return value is a file-like object. It also
157 has methods and attributes including:
157 has methods and attributes including:
158 - info(): return a mimetools.Message object for the headers
158 - info(): return a mimetools.Message object for the headers
159 - geturl(): return the original request URL
159 - geturl(): return the original request URL
160 - code: HTTP status code
160 - code: HTTP status code
161 """
161 """
162 # If using a proxy, the host returned by get_host() is
162 # If using a proxy, the host returned by get_host() is
163 # actually the proxy. On Python 2.6.1, the real destination
163 # actually the proxy. On Python 2.6.1, the real destination
164 # hostname is encoded in the URI in the urllib2 request
164 # hostname is encoded in the URI in the urllib2 request
165 # object. On Python 2.6.5, it's stored in the _tunnel_host
165 # object. On Python 2.6.5, it's stored in the _tunnel_host
166 # attribute which has no accessor.
166 # attribute which has no accessor.
167 tunhost = getattr(req, '_tunnel_host', None)
167 tunhost = getattr(req, '_tunnel_host', None)
168 host = req.get_host()
168 host = req.get_host()
169 if tunhost:
169 if tunhost:
170 proxyhost = host
170 proxyhost = host
171 host = tunhost
171 host = tunhost
172 elif req.has_proxy():
172 elif req.has_proxy():
173 proxyhost = req.get_host()
173 proxyhost = req.get_host()
174 host = req.get_selector().split('://', 1)[1].split('/', 1)[0]
174 host = req.get_selector().split('://', 1)[1].split('/', 1)[0]
175 else:
175 else:
176 proxyhost = None
176 proxyhost = None
177
177
178 if proxyhost:
178 if proxyhost:
179 if ':' in proxyhost:
179 if ':' in proxyhost:
180 # Note: this means we'll explode if we try and use an
180 # Note: this means we'll explode if we try and use an
181 # IPv6 http proxy. This isn't a regression, so we
181 # IPv6 http proxy. This isn't a regression, so we
182 # won't worry about it for now.
182 # won't worry about it for now.
183 proxyhost, proxyport = proxyhost.rsplit(':', 1)
183 proxyhost, proxyport = proxyhost.rsplit(':', 1)
184 else:
184 else:
185 proxyport = 3128 # squid default
185 proxyport = 3128 # squid default
186 proxy = (proxyhost, proxyport)
186 proxy = (proxyhost, proxyport)
187 else:
187 else:
188 proxy = None
188 proxy = None
189
189
190 if not host:
190 if not host:
191 raise urlerr.urlerror('no host given')
191 raise urlerr.urlerror('no host given')
192
192
193 connkey = use_ssl, host, proxy
193 connkey = use_ssl, host, proxy
194 allconns = self._connections.get(connkey, [])
194 allconns = self._connections.get(connkey, [])
195 conns = [c for c in allconns if not c.busy()]
195 conns = [c for c in allconns if not c.busy()]
196 if conns:
196 if conns:
197 h = conns[0]
197 h = conns[0]
198 else:
198 else:
199 if allconns:
199 if allconns:
200 self.ui.debug('all connections for %s busy, making a new '
200 self.ui.debug('all connections for %s busy, making a new '
201 'one\n' % host)
201 'one\n' % host)
202 timeout = None
202 timeout = None
203 if req.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
203 if req.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
204 timeout = req.timeout
204 timeout = req.timeout
205 h = http_class(host, timeout=timeout, proxy_hostport=proxy)
205 h = http_class(host, timeout=timeout, proxy_hostport=proxy)
206 self._connections.setdefault(connkey, []).append(h)
206 self._connections.setdefault(connkey, []).append(h)
207
207
208 headers = dict(req.headers)
208 headers = dict(req.headers)
209 headers.update(req.unredirected_hdrs)
209 headers.update(req.unredirected_hdrs)
210 headers = dict(
210 headers = dict(
211 (name.title(), val) for name, val in headers.items())
211 (name.title(), val) for name, val in headers.items())
212 try:
212 try:
213 path = req.get_selector()
213 path = req.get_selector()
214 if '://' in path:
214 if '://' in path:
215 path = path.split('://', 1)[1].split('/', 1)[1]
215 path = path.split('://', 1)[1].split('/', 1)[1]
216 if path[0] != '/':
216 if path[0] != '/':
217 path = '/' + path
217 path = '/' + path
218 h.request(req.get_method(), path, req.data, headers)
218 h.request(req.get_method(), path, req.data, headers)
219 r = h.getresponse()
219 r = h.getresponse()
220 except socket.error as err: # XXX what error?
220 except socket.error as err: # XXX what error?
221 raise urlerr.urlerror(err)
221 raise urlerr.urlerror(err)
222
222
223 # Pick apart the HTTPResponse object to get the addinfourl
223 # Pick apart the HTTPResponse object to get the addinfourl
224 # object initialized properly.
224 # object initialized properly.
225 r.recv = r.read
225 r.recv = r.read
226
226
227 resp = urlreq.addinfourl(r, r.headers, req.get_full_url())
227 resp = urlreq.addinfourl(r, r.headers, req.get_full_url())
228 resp.code = r.status
228 resp.code = r.status
229 resp.msg = r.reason
229 resp.msg = r.reason
230 return resp
230 return resp
231
231
232 # httplib always uses the given host/port as the socket connect
232 # httplib always uses the given host/port as the socket connect
233 # target, and then allows full URIs in the request path, which it
233 # target, and then allows full URIs in the request path, which it
234 # then observes and treats as a signal to do proxying instead.
234 # then observes and treats as a signal to do proxying instead.
235 def http_open(self, req):
235 def http_open(self, req):
236 if req.get_full_url().startswith('https'):
236 if req.get_full_url().startswith('https'):
237 return self.https_open(req)
237 return self.https_open(req)
238 def makehttpcon(*args, **kwargs):
238 def makehttpcon(*args, **kwargs):
239 k2 = dict(kwargs)
239 k2 = dict(kwargs)
240 k2['use_ssl'] = False
240 k2['use_ssl'] = False
241 return HTTPConnection(*args, **k2)
241 return HTTPConnection(*args, **k2)
242 return self.do_open(makehttpcon, req, False)
242 return self.do_open(makehttpcon, req, False)
243
243
244 def https_open(self, req):
244 def https_open(self, req):
245 # req.get_full_url() does not contain credentials and we may
245 # req.get_full_url() does not contain credentials and we may
246 # need them to match the certificates.
246 # need them to match the certificates.
247 url = req.get_full_url()
247 url = req.get_full_url()
248 user, password = self.pwmgr.find_stored_password(url)
248 user, password = self.pwmgr.find_stored_password(url)
249 res = readauthforuri(self.ui, url, user)
249 res = readauthforuri(self.ui, url, user)
250 if res:
250 if res:
251 group, auth = res
251 group, auth = res
252 self.auth = auth
252 self.auth = auth
253 self.ui.debug("using auth.%s.* for authentication\n" % group)
253 self.ui.debug("using auth.%s.* for authentication\n" % group)
254 else:
254 else:
255 self.auth = None
255 self.auth = None
256 return self.do_open(self._makesslconnection, req, True)
256 return self.do_open(self._makesslconnection, req, True)
257
257
258 def _makesslconnection(self, host, port=443, *args, **kwargs):
258 def _makesslconnection(self, host, port=443, *args, **kwargs):
259 keyfile = None
259 keyfile = None
260 certfile = None
260 certfile = None
261
261
262 if args: # key_file
262 if args: # key_file
263 keyfile = args.pop(0)
263 keyfile = args.pop(0)
264 if args: # cert_file
264 if args: # cert_file
265 certfile = args.pop(0)
265 certfile = args.pop(0)
266
266
267 # if the user has specified different key/cert files in
267 # if the user has specified different key/cert files in
268 # hgrc, we prefer these
268 # hgrc, we prefer these
269 if self.auth and 'key' in self.auth and 'cert' in self.auth:
269 if self.auth and 'key' in self.auth and 'cert' in self.auth:
270 keyfile = self.auth['key']
270 keyfile = self.auth['key']
271 certfile = self.auth['cert']
271 certfile = self.auth['cert']
272
272
273 # let host port take precedence
273 # let host port take precedence
274 if ':' in host and '[' not in host or ']:' in host:
274 if ':' in host and '[' not in host or ']:' in host:
275 host, port = host.rsplit(':', 1)
275 host, port = host.rsplit(':', 1)
276 port = int(port)
276 port = int(port)
277 if '[' in host:
277 if '[' in host:
278 host = host[1:-1]
278 host = host[1:-1]
279
279
280 kwargs['keyfile'] = keyfile
280 kwargs['keyfile'] = keyfile
281 kwargs['certfile'] = certfile
281 kwargs['certfile'] = certfile
282
282
283 kwargs.update(sslutil.sslkwargs(self.ui, host))
283 kwargs.update(sslutil.sslkwargs(self.ui, host))
284
284
285 con = HTTPConnection(host, port, use_ssl=True,
285 con = HTTPConnection(host, port, use_ssl=True,
286 ssl_wrap_socket=sslutil.wrapsocket,
286 ssl_wrap_socket=sslutil.wrapsocket,
287 ssl_validator=sslutil.validator(self.ui, host),
287 ssl_validator=sslutil.validatesocket,
288 **kwargs)
288 **kwargs)
289 return con
289 return con
@@ -1,357 +1,357 b''
1 # mail.py - mail sending bits for mercurial
1 # mail.py - mail sending bits for mercurial
2 #
2 #
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import email
10 import email
11 import os
11 import os
12 import quopri
12 import quopri
13 import smtplib
13 import smtplib
14 import socket
14 import socket
15 import sys
15 import sys
16 import time
16 import time
17
17
18 from .i18n import _
18 from .i18n import _
19 from . import (
19 from . import (
20 encoding,
20 encoding,
21 error,
21 error,
22 sslutil,
22 sslutil,
23 util,
23 util,
24 )
24 )
25
25
26 _oldheaderinit = email.Header.Header.__init__
26 _oldheaderinit = email.Header.Header.__init__
27 def _unifiedheaderinit(self, *args, **kw):
27 def _unifiedheaderinit(self, *args, **kw):
28 """
28 """
29 Python 2.7 introduces a backwards incompatible change
29 Python 2.7 introduces a backwards incompatible change
30 (Python issue1974, r70772) in email.Generator.Generator code:
30 (Python issue1974, r70772) in email.Generator.Generator code:
31 pre-2.7 code passed "continuation_ws='\t'" to the Header
31 pre-2.7 code passed "continuation_ws='\t'" to the Header
32 constructor, and 2.7 removed this parameter.
32 constructor, and 2.7 removed this parameter.
33
33
34 Default argument is continuation_ws=' ', which means that the
34 Default argument is continuation_ws=' ', which means that the
35 behavior is different in <2.7 and 2.7
35 behavior is different in <2.7 and 2.7
36
36
37 We consider the 2.7 behavior to be preferable, but need
37 We consider the 2.7 behavior to be preferable, but need
38 to have an unified behavior for versions 2.4 to 2.7
38 to have an unified behavior for versions 2.4 to 2.7
39 """
39 """
40 # override continuation_ws
40 # override continuation_ws
41 kw['continuation_ws'] = ' '
41 kw['continuation_ws'] = ' '
42 _oldheaderinit(self, *args, **kw)
42 _oldheaderinit(self, *args, **kw)
43
43
44 setattr(email.header.Header, '__init__', _unifiedheaderinit)
44 setattr(email.header.Header, '__init__', _unifiedheaderinit)
45
45
46 class STARTTLS(smtplib.SMTP):
46 class STARTTLS(smtplib.SMTP):
47 '''Derived class to verify the peer certificate for STARTTLS.
47 '''Derived class to verify the peer certificate for STARTTLS.
48
48
49 This class allows to pass any keyword arguments to SSL socket creation.
49 This class allows to pass any keyword arguments to SSL socket creation.
50 '''
50 '''
51 def __init__(self, sslkwargs, host=None, **kwargs):
51 def __init__(self, sslkwargs, host=None, **kwargs):
52 smtplib.SMTP.__init__(self, **kwargs)
52 smtplib.SMTP.__init__(self, **kwargs)
53 self._sslkwargs = sslkwargs
53 self._sslkwargs = sslkwargs
54 self._host = host
54 self._host = host
55
55
56 def starttls(self, keyfile=None, certfile=None):
56 def starttls(self, keyfile=None, certfile=None):
57 if not self.has_extn("starttls"):
57 if not self.has_extn("starttls"):
58 msg = "STARTTLS extension not supported by server"
58 msg = "STARTTLS extension not supported by server"
59 raise smtplib.SMTPException(msg)
59 raise smtplib.SMTPException(msg)
60 (resp, reply) = self.docmd("STARTTLS")
60 (resp, reply) = self.docmd("STARTTLS")
61 if resp == 220:
61 if resp == 220:
62 self.sock = sslutil.wrapsocket(self.sock, keyfile, certfile,
62 self.sock = sslutil.wrapsocket(self.sock, keyfile, certfile,
63 serverhostname=self._host,
63 serverhostname=self._host,
64 **self._sslkwargs)
64 **self._sslkwargs)
65 self.file = smtplib.SSLFakeFile(self.sock)
65 self.file = smtplib.SSLFakeFile(self.sock)
66 self.helo_resp = None
66 self.helo_resp = None
67 self.ehlo_resp = None
67 self.ehlo_resp = None
68 self.esmtp_features = {}
68 self.esmtp_features = {}
69 self.does_esmtp = 0
69 self.does_esmtp = 0
70 return (resp, reply)
70 return (resp, reply)
71
71
72 class SMTPS(smtplib.SMTP):
72 class SMTPS(smtplib.SMTP):
73 '''Derived class to verify the peer certificate for SMTPS.
73 '''Derived class to verify the peer certificate for SMTPS.
74
74
75 This class allows to pass any keyword arguments to SSL socket creation.
75 This class allows to pass any keyword arguments to SSL socket creation.
76 '''
76 '''
77 def __init__(self, sslkwargs, keyfile=None, certfile=None, host=None,
77 def __init__(self, sslkwargs, keyfile=None, certfile=None, host=None,
78 **kwargs):
78 **kwargs):
79 self.keyfile = keyfile
79 self.keyfile = keyfile
80 self.certfile = certfile
80 self.certfile = certfile
81 smtplib.SMTP.__init__(self, **kwargs)
81 smtplib.SMTP.__init__(self, **kwargs)
82 self._host = host
82 self._host = host
83 self.default_port = smtplib.SMTP_SSL_PORT
83 self.default_port = smtplib.SMTP_SSL_PORT
84 self._sslkwargs = sslkwargs
84 self._sslkwargs = sslkwargs
85
85
86 def _get_socket(self, host, port, timeout):
86 def _get_socket(self, host, port, timeout):
87 if self.debuglevel > 0:
87 if self.debuglevel > 0:
88 print('connect:', (host, port), file=sys.stderr)
88 print('connect:', (host, port), file=sys.stderr)
89 new_socket = socket.create_connection((host, port), timeout)
89 new_socket = socket.create_connection((host, port), timeout)
90 new_socket = sslutil.wrapsocket(new_socket,
90 new_socket = sslutil.wrapsocket(new_socket,
91 self.keyfile, self.certfile,
91 self.keyfile, self.certfile,
92 serverhostname=self._host,
92 serverhostname=self._host,
93 **self._sslkwargs)
93 **self._sslkwargs)
94 self.file = smtplib.SSLFakeFile(new_socket)
94 self.file = smtplib.SSLFakeFile(new_socket)
95 return new_socket
95 return new_socket
96
96
97 def _smtp(ui):
97 def _smtp(ui):
98 '''build an smtp connection and return a function to send mail'''
98 '''build an smtp connection and return a function to send mail'''
99 local_hostname = ui.config('smtp', 'local_hostname')
99 local_hostname = ui.config('smtp', 'local_hostname')
100 tls = ui.config('smtp', 'tls', 'none')
100 tls = ui.config('smtp', 'tls', 'none')
101 # backward compatible: when tls = true, we use starttls.
101 # backward compatible: when tls = true, we use starttls.
102 starttls = tls == 'starttls' or util.parsebool(tls)
102 starttls = tls == 'starttls' or util.parsebool(tls)
103 smtps = tls == 'smtps'
103 smtps = tls == 'smtps'
104 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
104 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
105 raise error.Abort(_("can't use TLS: Python SSL support not installed"))
105 raise error.Abort(_("can't use TLS: Python SSL support not installed"))
106 mailhost = ui.config('smtp', 'host')
106 mailhost = ui.config('smtp', 'host')
107 if not mailhost:
107 if not mailhost:
108 raise error.Abort(_('smtp.host not configured - cannot send mail'))
108 raise error.Abort(_('smtp.host not configured - cannot send mail'))
109 verifycert = ui.config('smtp', 'verifycert', 'strict')
109 verifycert = ui.config('smtp', 'verifycert', 'strict')
110 if verifycert not in ['strict', 'loose']:
110 if verifycert not in ['strict', 'loose']:
111 if util.parsebool(verifycert) is not False:
111 if util.parsebool(verifycert) is not False:
112 raise error.Abort(_('invalid smtp.verifycert configuration: %s')
112 raise error.Abort(_('invalid smtp.verifycert configuration: %s')
113 % (verifycert))
113 % (verifycert))
114 verifycert = False
114 verifycert = False
115 if (starttls or smtps) and verifycert:
115 if (starttls or smtps) and verifycert:
116 sslkwargs = sslutil.sslkwargs(ui, mailhost)
116 sslkwargs = sslutil.sslkwargs(ui, mailhost)
117 else:
117 else:
118 # 'ui' is required by sslutil.wrapsocket() and set by sslkwargs()
118 # 'ui' is required by sslutil.wrapsocket() and set by sslkwargs()
119 sslkwargs = {'ui': ui}
119 sslkwargs = {'ui': ui}
120 if smtps:
120 if smtps:
121 ui.note(_('(using smtps)\n'))
121 ui.note(_('(using smtps)\n'))
122 s = SMTPS(sslkwargs, local_hostname=local_hostname, host=mailhost)
122 s = SMTPS(sslkwargs, local_hostname=local_hostname, host=mailhost)
123 elif starttls:
123 elif starttls:
124 s = STARTTLS(sslkwargs, local_hostname=local_hostname, host=mailhost)
124 s = STARTTLS(sslkwargs, local_hostname=local_hostname, host=mailhost)
125 else:
125 else:
126 s = smtplib.SMTP(local_hostname=local_hostname)
126 s = smtplib.SMTP(local_hostname=local_hostname)
127 if smtps:
127 if smtps:
128 defaultport = 465
128 defaultport = 465
129 else:
129 else:
130 defaultport = 25
130 defaultport = 25
131 mailport = util.getport(ui.config('smtp', 'port', defaultport))
131 mailport = util.getport(ui.config('smtp', 'port', defaultport))
132 ui.note(_('sending mail: smtp host %s, port %d\n') %
132 ui.note(_('sending mail: smtp host %s, port %d\n') %
133 (mailhost, mailport))
133 (mailhost, mailport))
134 s.connect(host=mailhost, port=mailport)
134 s.connect(host=mailhost, port=mailport)
135 if starttls:
135 if starttls:
136 ui.note(_('(using starttls)\n'))
136 ui.note(_('(using starttls)\n'))
137 s.ehlo()
137 s.ehlo()
138 s.starttls()
138 s.starttls()
139 s.ehlo()
139 s.ehlo()
140 if (starttls or smtps) and verifycert:
140 if (starttls or smtps) and verifycert:
141 ui.note(_('(verifying remote certificate)\n'))
141 ui.note(_('(verifying remote certificate)\n'))
142 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
142 sslutil.validatesocket(s.sock, verifycert == 'strict')
143 username = ui.config('smtp', 'username')
143 username = ui.config('smtp', 'username')
144 password = ui.config('smtp', 'password')
144 password = ui.config('smtp', 'password')
145 if username and not password:
145 if username and not password:
146 password = ui.getpass()
146 password = ui.getpass()
147 if username and password:
147 if username and password:
148 ui.note(_('(authenticating to mail server as %s)\n') %
148 ui.note(_('(authenticating to mail server as %s)\n') %
149 (username))
149 (username))
150 try:
150 try:
151 s.login(username, password)
151 s.login(username, password)
152 except smtplib.SMTPException as inst:
152 except smtplib.SMTPException as inst:
153 raise error.Abort(inst)
153 raise error.Abort(inst)
154
154
155 def send(sender, recipients, msg):
155 def send(sender, recipients, msg):
156 try:
156 try:
157 return s.sendmail(sender, recipients, msg)
157 return s.sendmail(sender, recipients, msg)
158 except smtplib.SMTPRecipientsRefused as inst:
158 except smtplib.SMTPRecipientsRefused as inst:
159 recipients = [r[1] for r in inst.recipients.values()]
159 recipients = [r[1] for r in inst.recipients.values()]
160 raise error.Abort('\n' + '\n'.join(recipients))
160 raise error.Abort('\n' + '\n'.join(recipients))
161 except smtplib.SMTPException as inst:
161 except smtplib.SMTPException as inst:
162 raise error.Abort(inst)
162 raise error.Abort(inst)
163
163
164 return send
164 return send
165
165
166 def _sendmail(ui, sender, recipients, msg):
166 def _sendmail(ui, sender, recipients, msg):
167 '''send mail using sendmail.'''
167 '''send mail using sendmail.'''
168 program = ui.config('email', 'method', 'smtp')
168 program = ui.config('email', 'method', 'smtp')
169 cmdline = '%s -f %s %s' % (program, util.email(sender),
169 cmdline = '%s -f %s %s' % (program, util.email(sender),
170 ' '.join(map(util.email, recipients)))
170 ' '.join(map(util.email, recipients)))
171 ui.note(_('sending mail: %s\n') % cmdline)
171 ui.note(_('sending mail: %s\n') % cmdline)
172 fp = util.popen(cmdline, 'w')
172 fp = util.popen(cmdline, 'w')
173 fp.write(msg)
173 fp.write(msg)
174 ret = fp.close()
174 ret = fp.close()
175 if ret:
175 if ret:
176 raise error.Abort('%s %s' % (
176 raise error.Abort('%s %s' % (
177 os.path.basename(program.split(None, 1)[0]),
177 os.path.basename(program.split(None, 1)[0]),
178 util.explainexit(ret)[0]))
178 util.explainexit(ret)[0]))
179
179
180 def _mbox(mbox, sender, recipients, msg):
180 def _mbox(mbox, sender, recipients, msg):
181 '''write mails to mbox'''
181 '''write mails to mbox'''
182 fp = open(mbox, 'ab+')
182 fp = open(mbox, 'ab+')
183 # Should be time.asctime(), but Windows prints 2-characters day
183 # Should be time.asctime(), but Windows prints 2-characters day
184 # of month instead of one. Make them print the same thing.
184 # of month instead of one. Make them print the same thing.
185 date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime())
185 date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime())
186 fp.write('From %s %s\n' % (sender, date))
186 fp.write('From %s %s\n' % (sender, date))
187 fp.write(msg)
187 fp.write(msg)
188 fp.write('\n\n')
188 fp.write('\n\n')
189 fp.close()
189 fp.close()
190
190
191 def connect(ui, mbox=None):
191 def connect(ui, mbox=None):
192 '''make a mail connection. return a function to send mail.
192 '''make a mail connection. return a function to send mail.
193 call as sendmail(sender, list-of-recipients, msg).'''
193 call as sendmail(sender, list-of-recipients, msg).'''
194 if mbox:
194 if mbox:
195 open(mbox, 'wb').close()
195 open(mbox, 'wb').close()
196 return lambda s, r, m: _mbox(mbox, s, r, m)
196 return lambda s, r, m: _mbox(mbox, s, r, m)
197 if ui.config('email', 'method', 'smtp') == 'smtp':
197 if ui.config('email', 'method', 'smtp') == 'smtp':
198 return _smtp(ui)
198 return _smtp(ui)
199 return lambda s, r, m: _sendmail(ui, s, r, m)
199 return lambda s, r, m: _sendmail(ui, s, r, m)
200
200
201 def sendmail(ui, sender, recipients, msg, mbox=None):
201 def sendmail(ui, sender, recipients, msg, mbox=None):
202 send = connect(ui, mbox=mbox)
202 send = connect(ui, mbox=mbox)
203 return send(sender, recipients, msg)
203 return send(sender, recipients, msg)
204
204
205 def validateconfig(ui):
205 def validateconfig(ui):
206 '''determine if we have enough config data to try sending email.'''
206 '''determine if we have enough config data to try sending email.'''
207 method = ui.config('email', 'method', 'smtp')
207 method = ui.config('email', 'method', 'smtp')
208 if method == 'smtp':
208 if method == 'smtp':
209 if not ui.config('smtp', 'host'):
209 if not ui.config('smtp', 'host'):
210 raise error.Abort(_('smtp specified as email transport, '
210 raise error.Abort(_('smtp specified as email transport, '
211 'but no smtp host configured'))
211 'but no smtp host configured'))
212 else:
212 else:
213 if not util.findexe(method):
213 if not util.findexe(method):
214 raise error.Abort(_('%r specified as email transport, '
214 raise error.Abort(_('%r specified as email transport, '
215 'but not in PATH') % method)
215 'but not in PATH') % method)
216
216
217 def mimetextpatch(s, subtype='plain', display=False):
217 def mimetextpatch(s, subtype='plain', display=False):
218 '''Return MIME message suitable for a patch.
218 '''Return MIME message suitable for a patch.
219 Charset will be detected as utf-8 or (possibly fake) us-ascii.
219 Charset will be detected as utf-8 or (possibly fake) us-ascii.
220 Transfer encodings will be used if necessary.'''
220 Transfer encodings will be used if necessary.'''
221
221
222 cs = 'us-ascii'
222 cs = 'us-ascii'
223 if not display:
223 if not display:
224 try:
224 try:
225 s.decode('us-ascii')
225 s.decode('us-ascii')
226 except UnicodeDecodeError:
226 except UnicodeDecodeError:
227 try:
227 try:
228 s.decode('utf-8')
228 s.decode('utf-8')
229 cs = 'utf-8'
229 cs = 'utf-8'
230 except UnicodeDecodeError:
230 except UnicodeDecodeError:
231 # We'll go with us-ascii as a fallback.
231 # We'll go with us-ascii as a fallback.
232 pass
232 pass
233
233
234 return mimetextqp(s, subtype, cs)
234 return mimetextqp(s, subtype, cs)
235
235
236 def mimetextqp(body, subtype, charset):
236 def mimetextqp(body, subtype, charset):
237 '''Return MIME message.
237 '''Return MIME message.
238 Quoted-printable transfer encoding will be used if necessary.
238 Quoted-printable transfer encoding will be used if necessary.
239 '''
239 '''
240 enc = None
240 enc = None
241 for line in body.splitlines():
241 for line in body.splitlines():
242 if len(line) > 950:
242 if len(line) > 950:
243 body = quopri.encodestring(body)
243 body = quopri.encodestring(body)
244 enc = "quoted-printable"
244 enc = "quoted-printable"
245 break
245 break
246
246
247 msg = email.MIMEText.MIMEText(body, subtype, charset)
247 msg = email.MIMEText.MIMEText(body, subtype, charset)
248 if enc:
248 if enc:
249 del msg['Content-Transfer-Encoding']
249 del msg['Content-Transfer-Encoding']
250 msg['Content-Transfer-Encoding'] = enc
250 msg['Content-Transfer-Encoding'] = enc
251 return msg
251 return msg
252
252
253 def _charsets(ui):
253 def _charsets(ui):
254 '''Obtains charsets to send mail parts not containing patches.'''
254 '''Obtains charsets to send mail parts not containing patches.'''
255 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
255 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
256 fallbacks = [encoding.fallbackencoding.lower(),
256 fallbacks = [encoding.fallbackencoding.lower(),
257 encoding.encoding.lower(), 'utf-8']
257 encoding.encoding.lower(), 'utf-8']
258 for cs in fallbacks: # find unique charsets while keeping order
258 for cs in fallbacks: # find unique charsets while keeping order
259 if cs not in charsets:
259 if cs not in charsets:
260 charsets.append(cs)
260 charsets.append(cs)
261 return [cs for cs in charsets if not cs.endswith('ascii')]
261 return [cs for cs in charsets if not cs.endswith('ascii')]
262
262
263 def _encode(ui, s, charsets):
263 def _encode(ui, s, charsets):
264 '''Returns (converted) string, charset tuple.
264 '''Returns (converted) string, charset tuple.
265 Finds out best charset by cycling through sendcharsets in descending
265 Finds out best charset by cycling through sendcharsets in descending
266 order. Tries both encoding and fallbackencoding for input. Only as
266 order. Tries both encoding and fallbackencoding for input. Only as
267 last resort send as is in fake ascii.
267 last resort send as is in fake ascii.
268 Caveat: Do not use for mail parts containing patches!'''
268 Caveat: Do not use for mail parts containing patches!'''
269 try:
269 try:
270 s.decode('ascii')
270 s.decode('ascii')
271 except UnicodeDecodeError:
271 except UnicodeDecodeError:
272 sendcharsets = charsets or _charsets(ui)
272 sendcharsets = charsets or _charsets(ui)
273 for ics in (encoding.encoding, encoding.fallbackencoding):
273 for ics in (encoding.encoding, encoding.fallbackencoding):
274 try:
274 try:
275 u = s.decode(ics)
275 u = s.decode(ics)
276 except UnicodeDecodeError:
276 except UnicodeDecodeError:
277 continue
277 continue
278 for ocs in sendcharsets:
278 for ocs in sendcharsets:
279 try:
279 try:
280 return u.encode(ocs), ocs
280 return u.encode(ocs), ocs
281 except UnicodeEncodeError:
281 except UnicodeEncodeError:
282 pass
282 pass
283 except LookupError:
283 except LookupError:
284 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
284 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
285 # if ascii, or all conversion attempts fail, send (broken) ascii
285 # if ascii, or all conversion attempts fail, send (broken) ascii
286 return s, 'us-ascii'
286 return s, 'us-ascii'
287
287
288 def headencode(ui, s, charsets=None, display=False):
288 def headencode(ui, s, charsets=None, display=False):
289 '''Returns RFC-2047 compliant header from given string.'''
289 '''Returns RFC-2047 compliant header from given string.'''
290 if not display:
290 if not display:
291 # split into words?
291 # split into words?
292 s, cs = _encode(ui, s, charsets)
292 s, cs = _encode(ui, s, charsets)
293 return str(email.Header.Header(s, cs))
293 return str(email.Header.Header(s, cs))
294 return s
294 return s
295
295
296 def _addressencode(ui, name, addr, charsets=None):
296 def _addressencode(ui, name, addr, charsets=None):
297 name = headencode(ui, name, charsets)
297 name = headencode(ui, name, charsets)
298 try:
298 try:
299 acc, dom = addr.split('@')
299 acc, dom = addr.split('@')
300 acc = acc.encode('ascii')
300 acc = acc.encode('ascii')
301 dom = dom.decode(encoding.encoding).encode('idna')
301 dom = dom.decode(encoding.encoding).encode('idna')
302 addr = '%s@%s' % (acc, dom)
302 addr = '%s@%s' % (acc, dom)
303 except UnicodeDecodeError:
303 except UnicodeDecodeError:
304 raise error.Abort(_('invalid email address: %s') % addr)
304 raise error.Abort(_('invalid email address: %s') % addr)
305 except ValueError:
305 except ValueError:
306 try:
306 try:
307 # too strict?
307 # too strict?
308 addr = addr.encode('ascii')
308 addr = addr.encode('ascii')
309 except UnicodeDecodeError:
309 except UnicodeDecodeError:
310 raise error.Abort(_('invalid local address: %s') % addr)
310 raise error.Abort(_('invalid local address: %s') % addr)
311 return email.Utils.formataddr((name, addr))
311 return email.Utils.formataddr((name, addr))
312
312
313 def addressencode(ui, address, charsets=None, display=False):
313 def addressencode(ui, address, charsets=None, display=False):
314 '''Turns address into RFC-2047 compliant header.'''
314 '''Turns address into RFC-2047 compliant header.'''
315 if display or not address:
315 if display or not address:
316 return address or ''
316 return address or ''
317 name, addr = email.Utils.parseaddr(address)
317 name, addr = email.Utils.parseaddr(address)
318 return _addressencode(ui, name, addr, charsets)
318 return _addressencode(ui, name, addr, charsets)
319
319
320 def addrlistencode(ui, addrs, charsets=None, display=False):
320 def addrlistencode(ui, addrs, charsets=None, display=False):
321 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
321 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
322 A single element of input list may contain multiple addresses, but output
322 A single element of input list may contain multiple addresses, but output
323 always has one address per item'''
323 always has one address per item'''
324 if display:
324 if display:
325 return [a.strip() for a in addrs if a.strip()]
325 return [a.strip() for a in addrs if a.strip()]
326
326
327 result = []
327 result = []
328 for name, addr in email.Utils.getaddresses(addrs):
328 for name, addr in email.Utils.getaddresses(addrs):
329 if name or addr:
329 if name or addr:
330 result.append(_addressencode(ui, name, addr, charsets))
330 result.append(_addressencode(ui, name, addr, charsets))
331 return result
331 return result
332
332
333 def mimeencode(ui, s, charsets=None, display=False):
333 def mimeencode(ui, s, charsets=None, display=False):
334 '''creates mime text object, encodes it if needed, and sets
334 '''creates mime text object, encodes it if needed, and sets
335 charset and transfer-encoding accordingly.'''
335 charset and transfer-encoding accordingly.'''
336 cs = 'us-ascii'
336 cs = 'us-ascii'
337 if not display:
337 if not display:
338 s, cs = _encode(ui, s, charsets)
338 s, cs = _encode(ui, s, charsets)
339 return mimetextqp(s, 'plain', cs)
339 return mimetextqp(s, 'plain', cs)
340
340
341 def headdecode(s):
341 def headdecode(s):
342 '''Decodes RFC-2047 header'''
342 '''Decodes RFC-2047 header'''
343 uparts = []
343 uparts = []
344 for part, charset in email.Header.decode_header(s):
344 for part, charset in email.Header.decode_header(s):
345 if charset is not None:
345 if charset is not None:
346 try:
346 try:
347 uparts.append(part.decode(charset))
347 uparts.append(part.decode(charset))
348 continue
348 continue
349 except UnicodeDecodeError:
349 except UnicodeDecodeError:
350 pass
350 pass
351 try:
351 try:
352 uparts.append(part.decode('UTF-8'))
352 uparts.append(part.decode('UTF-8'))
353 continue
353 continue
354 except UnicodeDecodeError:
354 except UnicodeDecodeError:
355 pass
355 pass
356 uparts.append(part.decode('ISO-8859-1'))
356 uparts.append(part.decode('ISO-8859-1'))
357 return encoding.tolocal(u' '.join(uparts).encode('UTF-8'))
357 return encoding.tolocal(u' '.join(uparts).encode('UTF-8'))
@@ -1,367 +1,367 b''
1 # sslutil.py - SSL handling for mercurial
1 # sslutil.py - SSL handling for mercurial
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import os
12 import os
13 import ssl
13 import ssl
14 import sys
14 import sys
15
15
16 from .i18n import _
16 from .i18n import _
17 from . import (
17 from . import (
18 error,
18 error,
19 util,
19 util,
20 )
20 )
21
21
22 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
22 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
23 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
23 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
24 # all exposed via the "ssl" module.
24 # all exposed via the "ssl" module.
25 #
25 #
26 # Depending on the version of Python being used, SSL/TLS support is either
26 # Depending on the version of Python being used, SSL/TLS support is either
27 # modern/secure or legacy/insecure. Many operations in this module have
27 # modern/secure or legacy/insecure. Many operations in this module have
28 # separate code paths depending on support in Python.
28 # separate code paths depending on support in Python.
29
29
30 hassni = getattr(ssl, 'HAS_SNI', False)
30 hassni = getattr(ssl, 'HAS_SNI', False)
31
31
32 try:
32 try:
33 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
33 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
34 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
34 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
35 except AttributeError:
35 except AttributeError:
36 OP_NO_SSLv2 = 0x1000000
36 OP_NO_SSLv2 = 0x1000000
37 OP_NO_SSLv3 = 0x2000000
37 OP_NO_SSLv3 = 0x2000000
38
38
39 try:
39 try:
40 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
40 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
41 # SSL/TLS features are available.
41 # SSL/TLS features are available.
42 SSLContext = ssl.SSLContext
42 SSLContext = ssl.SSLContext
43 modernssl = True
43 modernssl = True
44 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
44 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
45 except AttributeError:
45 except AttributeError:
46 modernssl = False
46 modernssl = False
47 _canloaddefaultcerts = False
47 _canloaddefaultcerts = False
48
48
49 # We implement SSLContext using the interface from the standard library.
49 # We implement SSLContext using the interface from the standard library.
50 class SSLContext(object):
50 class SSLContext(object):
51 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
51 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
52 _supportsciphers = sys.version_info >= (2, 7)
52 _supportsciphers = sys.version_info >= (2, 7)
53
53
54 def __init__(self, protocol):
54 def __init__(self, protocol):
55 # From the public interface of SSLContext
55 # From the public interface of SSLContext
56 self.protocol = protocol
56 self.protocol = protocol
57 self.check_hostname = False
57 self.check_hostname = False
58 self.options = 0
58 self.options = 0
59 self.verify_mode = ssl.CERT_NONE
59 self.verify_mode = ssl.CERT_NONE
60
60
61 # Used by our implementation.
61 # Used by our implementation.
62 self._certfile = None
62 self._certfile = None
63 self._keyfile = None
63 self._keyfile = None
64 self._certpassword = None
64 self._certpassword = None
65 self._cacerts = None
65 self._cacerts = None
66 self._ciphers = None
66 self._ciphers = None
67
67
68 def load_cert_chain(self, certfile, keyfile=None, password=None):
68 def load_cert_chain(self, certfile, keyfile=None, password=None):
69 self._certfile = certfile
69 self._certfile = certfile
70 self._keyfile = keyfile
70 self._keyfile = keyfile
71 self._certpassword = password
71 self._certpassword = password
72
72
73 def load_default_certs(self, purpose=None):
73 def load_default_certs(self, purpose=None):
74 pass
74 pass
75
75
76 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
76 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
77 if capath:
77 if capath:
78 raise error.Abort('capath not supported')
78 raise error.Abort('capath not supported')
79 if cadata:
79 if cadata:
80 raise error.Abort('cadata not supported')
80 raise error.Abort('cadata not supported')
81
81
82 self._cacerts = cafile
82 self._cacerts = cafile
83
83
84 def set_ciphers(self, ciphers):
84 def set_ciphers(self, ciphers):
85 if not self._supportsciphers:
85 if not self._supportsciphers:
86 raise error.Abort('setting ciphers not supported')
86 raise error.Abort('setting ciphers not supported')
87
87
88 self._ciphers = ciphers
88 self._ciphers = ciphers
89
89
90 def wrap_socket(self, socket, server_hostname=None, server_side=False):
90 def wrap_socket(self, socket, server_hostname=None, server_side=False):
91 # server_hostname is unique to SSLContext.wrap_socket and is used
91 # server_hostname is unique to SSLContext.wrap_socket and is used
92 # for SNI in that context. So there's nothing for us to do with it
92 # for SNI in that context. So there's nothing for us to do with it
93 # in this legacy code since we don't support SNI.
93 # in this legacy code since we don't support SNI.
94
94
95 args = {
95 args = {
96 'keyfile': self._keyfile,
96 'keyfile': self._keyfile,
97 'certfile': self._certfile,
97 'certfile': self._certfile,
98 'server_side': server_side,
98 'server_side': server_side,
99 'cert_reqs': self.verify_mode,
99 'cert_reqs': self.verify_mode,
100 'ssl_version': self.protocol,
100 'ssl_version': self.protocol,
101 'ca_certs': self._cacerts,
101 'ca_certs': self._cacerts,
102 }
102 }
103
103
104 if self._supportsciphers:
104 if self._supportsciphers:
105 args['ciphers'] = self._ciphers
105 args['ciphers'] = self._ciphers
106
106
107 return ssl.wrap_socket(socket, **args)
107 return ssl.wrap_socket(socket, **args)
108
108
109 def wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE,
109 def wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE,
110 ca_certs=None, serverhostname=None):
110 ca_certs=None, serverhostname=None):
111 """Add SSL/TLS to a socket.
111 """Add SSL/TLS to a socket.
112
112
113 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
113 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
114 choices based on what security options are available.
114 choices based on what security options are available.
115
115
116 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
116 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
117 the following additional arguments:
117 the following additional arguments:
118
118
119 * serverhostname - The expected hostname of the remote server. If the
119 * serverhostname - The expected hostname of the remote server. If the
120 server (and client) support SNI, this tells the server which certificate
120 server (and client) support SNI, this tells the server which certificate
121 to use.
121 to use.
122 """
122 """
123 if not serverhostname:
123 if not serverhostname:
124 raise error.Abort('serverhostname argument is required')
124 raise error.Abort('serverhostname argument is required')
125
125
126 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
126 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
127 # that both ends support, including TLS protocols. On legacy stacks,
127 # that both ends support, including TLS protocols. On legacy stacks,
128 # the highest it likely goes in TLS 1.0. On modern stacks, it can
128 # the highest it likely goes in TLS 1.0. On modern stacks, it can
129 # support TLS 1.2.
129 # support TLS 1.2.
130 #
130 #
131 # The PROTOCOL_TLSv* constants select a specific TLS version
131 # The PROTOCOL_TLSv* constants select a specific TLS version
132 # only (as opposed to multiple versions). So the method for
132 # only (as opposed to multiple versions). So the method for
133 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
133 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
134 # disable protocols via SSLContext.options and OP_NO_* constants.
134 # disable protocols via SSLContext.options and OP_NO_* constants.
135 # However, SSLContext.options doesn't work unless we have the
135 # However, SSLContext.options doesn't work unless we have the
136 # full/real SSLContext available to us.
136 # full/real SSLContext available to us.
137 #
137 #
138 # SSLv2 and SSLv3 are broken. We ban them outright.
138 # SSLv2 and SSLv3 are broken. We ban them outright.
139 if modernssl:
139 if modernssl:
140 protocol = ssl.PROTOCOL_SSLv23
140 protocol = ssl.PROTOCOL_SSLv23
141 else:
141 else:
142 protocol = ssl.PROTOCOL_TLSv1
142 protocol = ssl.PROTOCOL_TLSv1
143
143
144 # TODO use ssl.create_default_context() on modernssl.
144 # TODO use ssl.create_default_context() on modernssl.
145 sslcontext = SSLContext(protocol)
145 sslcontext = SSLContext(protocol)
146
146
147 # This is a no-op on old Python.
147 # This is a no-op on old Python.
148 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
148 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
149
149
150 # This still works on our fake SSLContext.
150 # This still works on our fake SSLContext.
151 sslcontext.verify_mode = cert_reqs
151 sslcontext.verify_mode = cert_reqs
152
152
153 if certfile is not None:
153 if certfile is not None:
154 def password():
154 def password():
155 f = keyfile or certfile
155 f = keyfile or certfile
156 return ui.getpass(_('passphrase for %s: ') % f, '')
156 return ui.getpass(_('passphrase for %s: ') % f, '')
157 sslcontext.load_cert_chain(certfile, keyfile, password)
157 sslcontext.load_cert_chain(certfile, keyfile, password)
158
158
159 if ca_certs is not None:
159 if ca_certs is not None:
160 sslcontext.load_verify_locations(cafile=ca_certs)
160 sslcontext.load_verify_locations(cafile=ca_certs)
161 caloaded = True
161 caloaded = True
162 else:
162 else:
163 # This is a no-op on old Python.
163 # This is a no-op on old Python.
164 sslcontext.load_default_certs()
164 sslcontext.load_default_certs()
165 caloaded = _canloaddefaultcerts
165 caloaded = _canloaddefaultcerts
166
166
167 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
167 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
168 # check if wrap_socket failed silently because socket had been
168 # check if wrap_socket failed silently because socket had been
169 # closed
169 # closed
170 # - see http://bugs.python.org/issue13721
170 # - see http://bugs.python.org/issue13721
171 if not sslsocket.cipher():
171 if not sslsocket.cipher():
172 raise error.Abort(_('ssl connection failed'))
172 raise error.Abort(_('ssl connection failed'))
173
173
174 sslsocket._hgstate = {
174 sslsocket._hgstate = {
175 'caloaded': caloaded,
175 'caloaded': caloaded,
176 'hostname': serverhostname,
176 'hostname': serverhostname,
177 'ui': ui,
177 'ui': ui,
178 }
178 }
179
179
180 return sslsocket
180 return sslsocket
181
181
182 def _verifycert(cert, hostname):
182 def _verifycert(cert, hostname):
183 '''Verify that cert (in socket.getpeercert() format) matches hostname.
183 '''Verify that cert (in socket.getpeercert() format) matches hostname.
184 CRLs is not handled.
184 CRLs is not handled.
185
185
186 Returns error message if any problems are found and None on success.
186 Returns error message if any problems are found and None on success.
187 '''
187 '''
188 if not cert:
188 if not cert:
189 return _('no certificate received')
189 return _('no certificate received')
190 dnsname = hostname.lower()
190 dnsname = hostname.lower()
191 def matchdnsname(certname):
191 def matchdnsname(certname):
192 return (certname == dnsname or
192 return (certname == dnsname or
193 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
193 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
194
194
195 san = cert.get('subjectAltName', [])
195 san = cert.get('subjectAltName', [])
196 if san:
196 if san:
197 certnames = [value.lower() for key, value in san if key == 'DNS']
197 certnames = [value.lower() for key, value in san if key == 'DNS']
198 for name in certnames:
198 for name in certnames:
199 if matchdnsname(name):
199 if matchdnsname(name):
200 return None
200 return None
201 if certnames:
201 if certnames:
202 return _('certificate is for %s') % ', '.join(certnames)
202 return _('certificate is for %s') % ', '.join(certnames)
203
203
204 # subject is only checked when subjectAltName is empty
204 # subject is only checked when subjectAltName is empty
205 for s in cert.get('subject', []):
205 for s in cert.get('subject', []):
206 key, value = s[0]
206 key, value = s[0]
207 if key == 'commonName':
207 if key == 'commonName':
208 try:
208 try:
209 # 'subject' entries are unicode
209 # 'subject' entries are unicode
210 certname = value.lower().encode('ascii')
210 certname = value.lower().encode('ascii')
211 except UnicodeEncodeError:
211 except UnicodeEncodeError:
212 return _('IDN in certificate not supported')
212 return _('IDN in certificate not supported')
213 if matchdnsname(certname):
213 if matchdnsname(certname):
214 return None
214 return None
215 return _('certificate is for %s') % certname
215 return _('certificate is for %s') % certname
216 return _('no commonName or subjectAltName found in certificate')
216 return _('no commonName or subjectAltName found in certificate')
217
217
218
218
219 # CERT_REQUIRED means fetch the cert from the server all the time AND
219 # CERT_REQUIRED means fetch the cert from the server all the time AND
220 # validate it against the CA store provided in web.cacerts.
220 # validate it against the CA store provided in web.cacerts.
221
221
222 def _plainapplepython():
222 def _plainapplepython():
223 """return true if this seems to be a pure Apple Python that
223 """return true if this seems to be a pure Apple Python that
224 * is unfrozen and presumably has the whole mercurial module in the file
224 * is unfrozen and presumably has the whole mercurial module in the file
225 system
225 system
226 * presumably is an Apple Python that uses Apple OpenSSL which has patches
226 * presumably is an Apple Python that uses Apple OpenSSL which has patches
227 for using system certificate store CAs in addition to the provided
227 for using system certificate store CAs in addition to the provided
228 cacerts file
228 cacerts file
229 """
229 """
230 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
230 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
231 return False
231 return False
232 exe = os.path.realpath(sys.executable).lower()
232 exe = os.path.realpath(sys.executable).lower()
233 return (exe.startswith('/usr/bin/python') or
233 return (exe.startswith('/usr/bin/python') or
234 exe.startswith('/system/library/frameworks/python.framework/'))
234 exe.startswith('/system/library/frameworks/python.framework/'))
235
235
236 def _defaultcacerts():
236 def _defaultcacerts():
237 """return path to default CA certificates or None."""
237 """return path to default CA certificates or None."""
238 if _plainapplepython():
238 if _plainapplepython():
239 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
239 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
240 if os.path.exists(dummycert):
240 if os.path.exists(dummycert):
241 return dummycert
241 return dummycert
242
242
243 return None
243 return None
244
244
245 def sslkwargs(ui, host):
245 def sslkwargs(ui, host):
246 """Determine arguments to pass to wrapsocket().
246 """Determine arguments to pass to wrapsocket().
247
247
248 ``host`` is the hostname being connected to.
248 ``host`` is the hostname being connected to.
249 """
249 """
250 kws = {'ui': ui}
250 kws = {'ui': ui}
251
251
252 # If a host key fingerprint is on file, it is the only thing that matters
252 # If a host key fingerprint is on file, it is the only thing that matters
253 # and CA certs don't come into play.
253 # and CA certs don't come into play.
254 hostfingerprint = ui.config('hostfingerprints', host)
254 hostfingerprint = ui.config('hostfingerprints', host)
255 if hostfingerprint:
255 if hostfingerprint:
256 return kws
256 return kws
257
257
258 # The code below sets up CA verification arguments. If --insecure is
258 # The code below sets up CA verification arguments. If --insecure is
259 # used, we don't take CAs into consideration, so return early.
259 # used, we don't take CAs into consideration, so return early.
260 if ui.insecureconnections:
260 if ui.insecureconnections:
261 return kws
261 return kws
262
262
263 cacerts = ui.config('web', 'cacerts')
263 cacerts = ui.config('web', 'cacerts')
264
264
265 # If a value is set in the config, validate against a path and load
265 # If a value is set in the config, validate against a path and load
266 # and require those certs.
266 # and require those certs.
267 if cacerts:
267 if cacerts:
268 cacerts = util.expandpath(cacerts)
268 cacerts = util.expandpath(cacerts)
269 if not os.path.exists(cacerts):
269 if not os.path.exists(cacerts):
270 raise error.Abort(_('could not find web.cacerts: %s') % cacerts)
270 raise error.Abort(_('could not find web.cacerts: %s') % cacerts)
271
271
272 kws.update({'ca_certs': cacerts,
272 kws.update({'ca_certs': cacerts,
273 'cert_reqs': ssl.CERT_REQUIRED})
273 'cert_reqs': ssl.CERT_REQUIRED})
274 return kws
274 return kws
275
275
276 # No CAs in config. See if we can load defaults.
276 # No CAs in config. See if we can load defaults.
277 cacerts = _defaultcacerts()
277 cacerts = _defaultcacerts()
278
278
279 # We found an alternate CA bundle to use. Load it.
279 # We found an alternate CA bundle to use. Load it.
280 if cacerts:
280 if cacerts:
281 ui.debug('using %s to enable OS X system CA\n' % cacerts)
281 ui.debug('using %s to enable OS X system CA\n' % cacerts)
282 ui.setconfig('web', 'cacerts', cacerts, 'defaultcacerts')
282 ui.setconfig('web', 'cacerts', cacerts, 'defaultcacerts')
283 kws.update({'ca_certs': cacerts,
283 kws.update({'ca_certs': cacerts,
284 'cert_reqs': ssl.CERT_REQUIRED})
284 'cert_reqs': ssl.CERT_REQUIRED})
285 return kws
285 return kws
286
286
287 # FUTURE this can disappear once wrapsocket() is secure by default.
287 # FUTURE this can disappear once wrapsocket() is secure by default.
288 if _canloaddefaultcerts:
288 if _canloaddefaultcerts:
289 kws['cert_reqs'] = ssl.CERT_REQUIRED
289 kws['cert_reqs'] = ssl.CERT_REQUIRED
290 return kws
290 return kws
291
291
292 return kws
292 return kws
293
293
294 class validator(object):
294 def validatesocket(sock, strict=False):
295 def __init__(self, ui=None, host=None):
295 """Validate a socket meets security requiremnets.
296 pass
297
296
298 def __call__(self, sock, strict=False):
297 The passed socket must have been created with ``wrapsocket()``.
299 host = sock._hgstate['hostname']
298 """
300 ui = sock._hgstate['ui']
299 host = sock._hgstate['hostname']
300 ui = sock._hgstate['ui']
301
301
302 if not sock.cipher(): # work around http://bugs.python.org/issue13721
302 if not sock.cipher(): # work around http://bugs.python.org/issue13721
303 raise error.Abort(_('%s ssl connection error') % host)
303 raise error.Abort(_('%s ssl connection error') % host)
304 try:
304 try:
305 peercert = sock.getpeercert(True)
305 peercert = sock.getpeercert(True)
306 peercert2 = sock.getpeercert()
306 peercert2 = sock.getpeercert()
307 except AttributeError:
307 except AttributeError:
308 raise error.Abort(_('%s ssl connection error') % host)
308 raise error.Abort(_('%s ssl connection error') % host)
309
309
310 if not peercert:
310 if not peercert:
311 raise error.Abort(_('%s certificate error: '
311 raise error.Abort(_('%s certificate error: '
312 'no certificate received') % host)
312 'no certificate received') % host)
313
313
314 # If a certificate fingerprint is pinned, use it and only it to
314 # If a certificate fingerprint is pinned, use it and only it to
315 # validate the remote cert.
315 # validate the remote cert.
316 hostfingerprints = ui.configlist('hostfingerprints', host)
316 hostfingerprints = ui.configlist('hostfingerprints', host)
317 peerfingerprint = util.sha1(peercert).hexdigest()
317 peerfingerprint = util.sha1(peercert).hexdigest()
318 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
318 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
319 for x in xrange(0, len(peerfingerprint), 2)])
319 for x in xrange(0, len(peerfingerprint), 2)])
320 if hostfingerprints:
320 if hostfingerprints:
321 fingerprintmatch = False
321 fingerprintmatch = False
322 for hostfingerprint in hostfingerprints:
322 for hostfingerprint in hostfingerprints:
323 if peerfingerprint.lower() == \
323 if peerfingerprint.lower() == \
324 hostfingerprint.replace(':', '').lower():
324 hostfingerprint.replace(':', '').lower():
325 fingerprintmatch = True
325 fingerprintmatch = True
326 break
326 break
327 if not fingerprintmatch:
327 if not fingerprintmatch:
328 raise error.Abort(_('certificate for %s has unexpected '
328 raise error.Abort(_('certificate for %s has unexpected '
329 'fingerprint %s') % (host, nicefingerprint),
329 'fingerprint %s') % (host, nicefingerprint),
330 hint=_('check hostfingerprint configuration'))
330 hint=_('check hostfingerprint configuration'))
331 ui.debug('%s certificate matched fingerprint %s\n' %
331 ui.debug('%s certificate matched fingerprint %s\n' %
332 (host, nicefingerprint))
332 (host, nicefingerprint))
333 return
333 return
334
334
335 # If insecure connections were explicitly requested via --insecure,
335 # If insecure connections were explicitly requested via --insecure,
336 # print a warning and do no verification.
336 # print a warning and do no verification.
337 #
337 #
338 # It may seem odd that this is checked *after* host fingerprint pinning.
338 # It may seem odd that this is checked *after* host fingerprint pinning.
339 # This is for backwards compatibility (for now). The message is also
339 # This is for backwards compatibility (for now). The message is also
340 # the same as below for BC.
340 # the same as below for BC.
341 if ui.insecureconnections:
341 if ui.insecureconnections:
342 ui.warn(_('warning: %s certificate with fingerprint %s not '
342 ui.warn(_('warning: %s certificate with fingerprint %s not '
343 'verified (check hostfingerprints or web.cacerts '
343 'verified (check hostfingerprints or web.cacerts '
344 'config setting)\n') %
344 'config setting)\n') %
345 (host, nicefingerprint))
345 (host, nicefingerprint))
346 return
346 return
347
347
348 if not sock._hgstate['caloaded']:
348 if not sock._hgstate['caloaded']:
349 if strict:
349 if strict:
350 raise error.Abort(_('%s certificate with fingerprint %s not '
350 raise error.Abort(_('%s certificate with fingerprint %s not '
351 'verified') % (host, nicefingerprint),
351 'verified') % (host, nicefingerprint),
352 hint=_('check hostfingerprints or '
352 hint=_('check hostfingerprints or '
353 'web.cacerts config setting'))
353 'web.cacerts config setting'))
354 else:
354 else:
355 ui.warn(_('warning: %s certificate with fingerprint %s '
355 ui.warn(_('warning: %s certificate with fingerprint %s '
356 'not verified (check hostfingerprints or '
356 'not verified (check hostfingerprints or '
357 'web.cacerts config setting)\n') %
357 'web.cacerts config setting)\n') %
358 (host, nicefingerprint))
358 (host, nicefingerprint))
359
359
360 return
360 return
361
361
362 msg = _verifycert(peercert2, host)
362 msg = _verifycert(peercert2, host)
363 if msg:
363 if msg:
364 raise error.Abort(_('%s certificate error: %s') % (host, msg),
364 raise error.Abort(_('%s certificate error: %s') % (host, msg),
365 hint=_('configure hostfingerprint %s or use '
365 hint=_('configure hostfingerprint %s or use '
366 '--insecure to connect insecurely') %
366 '--insecure to connect insecurely') %
367 nicefingerprint)
367 nicefingerprint)
@@ -1,514 +1,514 b''
1 # url.py - HTTP handling for mercurial
1 # url.py - HTTP handling for mercurial
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import base64
12 import base64
13 import httplib
13 import httplib
14 import os
14 import os
15 import socket
15 import socket
16
16
17 from .i18n import _
17 from .i18n import _
18 from . import (
18 from . import (
19 error,
19 error,
20 httpconnection as httpconnectionmod,
20 httpconnection as httpconnectionmod,
21 keepalive,
21 keepalive,
22 sslutil,
22 sslutil,
23 util,
23 util,
24 )
24 )
25 stringio = util.stringio
25 stringio = util.stringio
26
26
27 urlerr = util.urlerr
27 urlerr = util.urlerr
28 urlreq = util.urlreq
28 urlreq = util.urlreq
29
29
30 class passwordmgr(urlreq.httppasswordmgrwithdefaultrealm):
30 class passwordmgr(urlreq.httppasswordmgrwithdefaultrealm):
31 def __init__(self, ui):
31 def __init__(self, ui):
32 urlreq.httppasswordmgrwithdefaultrealm.__init__(self)
32 urlreq.httppasswordmgrwithdefaultrealm.__init__(self)
33 self.ui = ui
33 self.ui = ui
34
34
35 def find_user_password(self, realm, authuri):
35 def find_user_password(self, realm, authuri):
36 authinfo = urlreq.httppasswordmgrwithdefaultrealm.find_user_password(
36 authinfo = urlreq.httppasswordmgrwithdefaultrealm.find_user_password(
37 self, realm, authuri)
37 self, realm, authuri)
38 user, passwd = authinfo
38 user, passwd = authinfo
39 if user and passwd:
39 if user and passwd:
40 self._writedebug(user, passwd)
40 self._writedebug(user, passwd)
41 return (user, passwd)
41 return (user, passwd)
42
42
43 if not user or not passwd:
43 if not user or not passwd:
44 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
44 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
45 if res:
45 if res:
46 group, auth = res
46 group, auth = res
47 user, passwd = auth.get('username'), auth.get('password')
47 user, passwd = auth.get('username'), auth.get('password')
48 self.ui.debug("using auth.%s.* for authentication\n" % group)
48 self.ui.debug("using auth.%s.* for authentication\n" % group)
49 if not user or not passwd:
49 if not user or not passwd:
50 u = util.url(authuri)
50 u = util.url(authuri)
51 u.query = None
51 u.query = None
52 if not self.ui.interactive():
52 if not self.ui.interactive():
53 raise error.Abort(_('http authorization required for %s') %
53 raise error.Abort(_('http authorization required for %s') %
54 util.hidepassword(str(u)))
54 util.hidepassword(str(u)))
55
55
56 self.ui.write(_("http authorization required for %s\n") %
56 self.ui.write(_("http authorization required for %s\n") %
57 util.hidepassword(str(u)))
57 util.hidepassword(str(u)))
58 self.ui.write(_("realm: %s\n") % realm)
58 self.ui.write(_("realm: %s\n") % realm)
59 if user:
59 if user:
60 self.ui.write(_("user: %s\n") % user)
60 self.ui.write(_("user: %s\n") % user)
61 else:
61 else:
62 user = self.ui.prompt(_("user:"), default=None)
62 user = self.ui.prompt(_("user:"), default=None)
63
63
64 if not passwd:
64 if not passwd:
65 passwd = self.ui.getpass()
65 passwd = self.ui.getpass()
66
66
67 self.add_password(realm, authuri, user, passwd)
67 self.add_password(realm, authuri, user, passwd)
68 self._writedebug(user, passwd)
68 self._writedebug(user, passwd)
69 return (user, passwd)
69 return (user, passwd)
70
70
71 def _writedebug(self, user, passwd):
71 def _writedebug(self, user, passwd):
72 msg = _('http auth: user %s, password %s\n')
72 msg = _('http auth: user %s, password %s\n')
73 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
73 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
74
74
75 def find_stored_password(self, authuri):
75 def find_stored_password(self, authuri):
76 return urlreq.httppasswordmgrwithdefaultrealm.find_user_password(
76 return urlreq.httppasswordmgrwithdefaultrealm.find_user_password(
77 self, None, authuri)
77 self, None, authuri)
78
78
79 class proxyhandler(urlreq.proxyhandler):
79 class proxyhandler(urlreq.proxyhandler):
80 def __init__(self, ui):
80 def __init__(self, ui):
81 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
81 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
82 # XXX proxyauthinfo = None
82 # XXX proxyauthinfo = None
83
83
84 if proxyurl:
84 if proxyurl:
85 # proxy can be proper url or host[:port]
85 # proxy can be proper url or host[:port]
86 if not (proxyurl.startswith('http:') or
86 if not (proxyurl.startswith('http:') or
87 proxyurl.startswith('https:')):
87 proxyurl.startswith('https:')):
88 proxyurl = 'http://' + proxyurl + '/'
88 proxyurl = 'http://' + proxyurl + '/'
89 proxy = util.url(proxyurl)
89 proxy = util.url(proxyurl)
90 if not proxy.user:
90 if not proxy.user:
91 proxy.user = ui.config("http_proxy", "user")
91 proxy.user = ui.config("http_proxy", "user")
92 proxy.passwd = ui.config("http_proxy", "passwd")
92 proxy.passwd = ui.config("http_proxy", "passwd")
93
93
94 # see if we should use a proxy for this url
94 # see if we should use a proxy for this url
95 no_list = ["localhost", "127.0.0.1"]
95 no_list = ["localhost", "127.0.0.1"]
96 no_list.extend([p.lower() for
96 no_list.extend([p.lower() for
97 p in ui.configlist("http_proxy", "no")])
97 p in ui.configlist("http_proxy", "no")])
98 no_list.extend([p.strip().lower() for
98 no_list.extend([p.strip().lower() for
99 p in os.getenv("no_proxy", '').split(',')
99 p in os.getenv("no_proxy", '').split(',')
100 if p.strip()])
100 if p.strip()])
101 # "http_proxy.always" config is for running tests on localhost
101 # "http_proxy.always" config is for running tests on localhost
102 if ui.configbool("http_proxy", "always"):
102 if ui.configbool("http_proxy", "always"):
103 self.no_list = []
103 self.no_list = []
104 else:
104 else:
105 self.no_list = no_list
105 self.no_list = no_list
106
106
107 proxyurl = str(proxy)
107 proxyurl = str(proxy)
108 proxies = {'http': proxyurl, 'https': proxyurl}
108 proxies = {'http': proxyurl, 'https': proxyurl}
109 ui.debug('proxying through http://%s:%s\n' %
109 ui.debug('proxying through http://%s:%s\n' %
110 (proxy.host, proxy.port))
110 (proxy.host, proxy.port))
111 else:
111 else:
112 proxies = {}
112 proxies = {}
113
113
114 # urllib2 takes proxy values from the environment and those
114 # urllib2 takes proxy values from the environment and those
115 # will take precedence if found. So, if there's a config entry
115 # will take precedence if found. So, if there's a config entry
116 # defining a proxy, drop the environment ones
116 # defining a proxy, drop the environment ones
117 if ui.config("http_proxy", "host"):
117 if ui.config("http_proxy", "host"):
118 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
118 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
119 try:
119 try:
120 if env in os.environ:
120 if env in os.environ:
121 del os.environ[env]
121 del os.environ[env]
122 except OSError:
122 except OSError:
123 pass
123 pass
124
124
125 urlreq.proxyhandler.__init__(self, proxies)
125 urlreq.proxyhandler.__init__(self, proxies)
126 self.ui = ui
126 self.ui = ui
127
127
128 def proxy_open(self, req, proxy, type_):
128 def proxy_open(self, req, proxy, type_):
129 host = req.get_host().split(':')[0]
129 host = req.get_host().split(':')[0]
130 for e in self.no_list:
130 for e in self.no_list:
131 if host == e:
131 if host == e:
132 return None
132 return None
133 if e.startswith('*.') and host.endswith(e[2:]):
133 if e.startswith('*.') and host.endswith(e[2:]):
134 return None
134 return None
135 if e.startswith('.') and host.endswith(e[1:]):
135 if e.startswith('.') and host.endswith(e[1:]):
136 return None
136 return None
137
137
138 return urlreq.proxyhandler.proxy_open(self, req, proxy, type_)
138 return urlreq.proxyhandler.proxy_open(self, req, proxy, type_)
139
139
140 def _gen_sendfile(orgsend):
140 def _gen_sendfile(orgsend):
141 def _sendfile(self, data):
141 def _sendfile(self, data):
142 # send a file
142 # send a file
143 if isinstance(data, httpconnectionmod.httpsendfile):
143 if isinstance(data, httpconnectionmod.httpsendfile):
144 # if auth required, some data sent twice, so rewind here
144 # if auth required, some data sent twice, so rewind here
145 data.seek(0)
145 data.seek(0)
146 for chunk in util.filechunkiter(data):
146 for chunk in util.filechunkiter(data):
147 orgsend(self, chunk)
147 orgsend(self, chunk)
148 else:
148 else:
149 orgsend(self, data)
149 orgsend(self, data)
150 return _sendfile
150 return _sendfile
151
151
152 has_https = util.safehasattr(urlreq, 'httpshandler')
152 has_https = util.safehasattr(urlreq, 'httpshandler')
153 if has_https:
153 if has_https:
154 try:
154 try:
155 _create_connection = socket.create_connection
155 _create_connection = socket.create_connection
156 except AttributeError:
156 except AttributeError:
157 _GLOBAL_DEFAULT_TIMEOUT = object()
157 _GLOBAL_DEFAULT_TIMEOUT = object()
158
158
159 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
159 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
160 source_address=None):
160 source_address=None):
161 # lifted from Python 2.6
161 # lifted from Python 2.6
162
162
163 msg = "getaddrinfo returns an empty list"
163 msg = "getaddrinfo returns an empty list"
164 host, port = address
164 host, port = address
165 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
165 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
166 af, socktype, proto, canonname, sa = res
166 af, socktype, proto, canonname, sa = res
167 sock = None
167 sock = None
168 try:
168 try:
169 sock = socket.socket(af, socktype, proto)
169 sock = socket.socket(af, socktype, proto)
170 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
170 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
171 sock.settimeout(timeout)
171 sock.settimeout(timeout)
172 if source_address:
172 if source_address:
173 sock.bind(source_address)
173 sock.bind(source_address)
174 sock.connect(sa)
174 sock.connect(sa)
175 return sock
175 return sock
176
176
177 except socket.error as msg:
177 except socket.error as msg:
178 if sock is not None:
178 if sock is not None:
179 sock.close()
179 sock.close()
180
180
181 raise socket.error(msg)
181 raise socket.error(msg)
182
182
183 class httpconnection(keepalive.HTTPConnection):
183 class httpconnection(keepalive.HTTPConnection):
184 # must be able to send big bundle as stream.
184 # must be able to send big bundle as stream.
185 send = _gen_sendfile(keepalive.HTTPConnection.send)
185 send = _gen_sendfile(keepalive.HTTPConnection.send)
186
186
187 def connect(self):
187 def connect(self):
188 if has_https and self.realhostport: # use CONNECT proxy
188 if has_https and self.realhostport: # use CONNECT proxy
189 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
189 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
190 self.sock.connect((self.host, self.port))
190 self.sock.connect((self.host, self.port))
191 if _generic_proxytunnel(self):
191 if _generic_proxytunnel(self):
192 # we do not support client X.509 certificates
192 # we do not support client X.509 certificates
193 self.sock = sslutil.wrapsocket(self.sock, None, None, None,
193 self.sock = sslutil.wrapsocket(self.sock, None, None, None,
194 serverhostname=self.host)
194 serverhostname=self.host)
195 else:
195 else:
196 keepalive.HTTPConnection.connect(self)
196 keepalive.HTTPConnection.connect(self)
197
197
198 def getresponse(self):
198 def getresponse(self):
199 proxyres = getattr(self, 'proxyres', None)
199 proxyres = getattr(self, 'proxyres', None)
200 if proxyres:
200 if proxyres:
201 if proxyres.will_close:
201 if proxyres.will_close:
202 self.close()
202 self.close()
203 self.proxyres = None
203 self.proxyres = None
204 return proxyres
204 return proxyres
205 return keepalive.HTTPConnection.getresponse(self)
205 return keepalive.HTTPConnection.getresponse(self)
206
206
207 # general transaction handler to support different ways to handle
207 # general transaction handler to support different ways to handle
208 # HTTPS proxying before and after Python 2.6.3.
208 # HTTPS proxying before and after Python 2.6.3.
209 def _generic_start_transaction(handler, h, req):
209 def _generic_start_transaction(handler, h, req):
210 tunnel_host = getattr(req, '_tunnel_host', None)
210 tunnel_host = getattr(req, '_tunnel_host', None)
211 if tunnel_host:
211 if tunnel_host:
212 if tunnel_host[:7] not in ['http://', 'https:/']:
212 if tunnel_host[:7] not in ['http://', 'https:/']:
213 tunnel_host = 'https://' + tunnel_host
213 tunnel_host = 'https://' + tunnel_host
214 new_tunnel = True
214 new_tunnel = True
215 else:
215 else:
216 tunnel_host = req.get_selector()
216 tunnel_host = req.get_selector()
217 new_tunnel = False
217 new_tunnel = False
218
218
219 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
219 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
220 u = util.url(tunnel_host)
220 u = util.url(tunnel_host)
221 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
221 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
222 h.realhostport = ':'.join([u.host, (u.port or '443')])
222 h.realhostport = ':'.join([u.host, (u.port or '443')])
223 h.headers = req.headers.copy()
223 h.headers = req.headers.copy()
224 h.headers.update(handler.parent.addheaders)
224 h.headers.update(handler.parent.addheaders)
225 return
225 return
226
226
227 h.realhostport = None
227 h.realhostport = None
228 h.headers = None
228 h.headers = None
229
229
230 def _generic_proxytunnel(self):
230 def _generic_proxytunnel(self):
231 proxyheaders = dict(
231 proxyheaders = dict(
232 [(x, self.headers[x]) for x in self.headers
232 [(x, self.headers[x]) for x in self.headers
233 if x.lower().startswith('proxy-')])
233 if x.lower().startswith('proxy-')])
234 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
234 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
235 for header in proxyheaders.iteritems():
235 for header in proxyheaders.iteritems():
236 self.send('%s: %s\r\n' % header)
236 self.send('%s: %s\r\n' % header)
237 self.send('\r\n')
237 self.send('\r\n')
238
238
239 # majority of the following code is duplicated from
239 # majority of the following code is duplicated from
240 # httplib.HTTPConnection as there are no adequate places to
240 # httplib.HTTPConnection as there are no adequate places to
241 # override functions to provide the needed functionality
241 # override functions to provide the needed functionality
242 res = self.response_class(self.sock,
242 res = self.response_class(self.sock,
243 strict=self.strict,
243 strict=self.strict,
244 method=self._method)
244 method=self._method)
245
245
246 while True:
246 while True:
247 version, status, reason = res._read_status()
247 version, status, reason = res._read_status()
248 if status != httplib.CONTINUE:
248 if status != httplib.CONTINUE:
249 break
249 break
250 while True:
250 while True:
251 skip = res.fp.readline().strip()
251 skip = res.fp.readline().strip()
252 if not skip:
252 if not skip:
253 break
253 break
254 res.status = status
254 res.status = status
255 res.reason = reason.strip()
255 res.reason = reason.strip()
256
256
257 if res.status == 200:
257 if res.status == 200:
258 while True:
258 while True:
259 line = res.fp.readline()
259 line = res.fp.readline()
260 if line == '\r\n':
260 if line == '\r\n':
261 break
261 break
262 return True
262 return True
263
263
264 if version == 'HTTP/1.0':
264 if version == 'HTTP/1.0':
265 res.version = 10
265 res.version = 10
266 elif version.startswith('HTTP/1.'):
266 elif version.startswith('HTTP/1.'):
267 res.version = 11
267 res.version = 11
268 elif version == 'HTTP/0.9':
268 elif version == 'HTTP/0.9':
269 res.version = 9
269 res.version = 9
270 else:
270 else:
271 raise httplib.UnknownProtocol(version)
271 raise httplib.UnknownProtocol(version)
272
272
273 if res.version == 9:
273 if res.version == 9:
274 res.length = None
274 res.length = None
275 res.chunked = 0
275 res.chunked = 0
276 res.will_close = 1
276 res.will_close = 1
277 res.msg = httplib.HTTPMessage(stringio())
277 res.msg = httplib.HTTPMessage(stringio())
278 return False
278 return False
279
279
280 res.msg = httplib.HTTPMessage(res.fp)
280 res.msg = httplib.HTTPMessage(res.fp)
281 res.msg.fp = None
281 res.msg.fp = None
282
282
283 # are we using the chunked-style of transfer encoding?
283 # are we using the chunked-style of transfer encoding?
284 trenc = res.msg.getheader('transfer-encoding')
284 trenc = res.msg.getheader('transfer-encoding')
285 if trenc and trenc.lower() == "chunked":
285 if trenc and trenc.lower() == "chunked":
286 res.chunked = 1
286 res.chunked = 1
287 res.chunk_left = None
287 res.chunk_left = None
288 else:
288 else:
289 res.chunked = 0
289 res.chunked = 0
290
290
291 # will the connection close at the end of the response?
291 # will the connection close at the end of the response?
292 res.will_close = res._check_close()
292 res.will_close = res._check_close()
293
293
294 # do we have a Content-Length?
294 # do we have a Content-Length?
295 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
295 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
296 # transfer-encoding is "chunked"
296 # transfer-encoding is "chunked"
297 length = res.msg.getheader('content-length')
297 length = res.msg.getheader('content-length')
298 if length and not res.chunked:
298 if length and not res.chunked:
299 try:
299 try:
300 res.length = int(length)
300 res.length = int(length)
301 except ValueError:
301 except ValueError:
302 res.length = None
302 res.length = None
303 else:
303 else:
304 if res.length < 0: # ignore nonsensical negative lengths
304 if res.length < 0: # ignore nonsensical negative lengths
305 res.length = None
305 res.length = None
306 else:
306 else:
307 res.length = None
307 res.length = None
308
308
309 # does the body have a fixed length? (of zero)
309 # does the body have a fixed length? (of zero)
310 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
310 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
311 100 <= status < 200 or # 1xx codes
311 100 <= status < 200 or # 1xx codes
312 res._method == 'HEAD'):
312 res._method == 'HEAD'):
313 res.length = 0
313 res.length = 0
314
314
315 # if the connection remains open, and we aren't using chunked, and
315 # if the connection remains open, and we aren't using chunked, and
316 # a content-length was not provided, then assume that the connection
316 # a content-length was not provided, then assume that the connection
317 # WILL close.
317 # WILL close.
318 if (not res.will_close and
318 if (not res.will_close and
319 not res.chunked and
319 not res.chunked and
320 res.length is None):
320 res.length is None):
321 res.will_close = 1
321 res.will_close = 1
322
322
323 self.proxyres = res
323 self.proxyres = res
324
324
325 return False
325 return False
326
326
327 class httphandler(keepalive.HTTPHandler):
327 class httphandler(keepalive.HTTPHandler):
328 def http_open(self, req):
328 def http_open(self, req):
329 return self.do_open(httpconnection, req)
329 return self.do_open(httpconnection, req)
330
330
331 def _start_transaction(self, h, req):
331 def _start_transaction(self, h, req):
332 _generic_start_transaction(self, h, req)
332 _generic_start_transaction(self, h, req)
333 return keepalive.HTTPHandler._start_transaction(self, h, req)
333 return keepalive.HTTPHandler._start_transaction(self, h, req)
334
334
335 if has_https:
335 if has_https:
336 class httpsconnection(httplib.HTTPConnection):
336 class httpsconnection(httplib.HTTPConnection):
337 response_class = keepalive.HTTPResponse
337 response_class = keepalive.HTTPResponse
338 default_port = httplib.HTTPS_PORT
338 default_port = httplib.HTTPS_PORT
339 # must be able to send big bundle as stream.
339 # must be able to send big bundle as stream.
340 send = _gen_sendfile(keepalive.safesend)
340 send = _gen_sendfile(keepalive.safesend)
341 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
341 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
342
342
343 def __init__(self, host, port=None, key_file=None, cert_file=None,
343 def __init__(self, host, port=None, key_file=None, cert_file=None,
344 *args, **kwargs):
344 *args, **kwargs):
345 httplib.HTTPConnection.__init__(self, host, port, *args, **kwargs)
345 httplib.HTTPConnection.__init__(self, host, port, *args, **kwargs)
346 self.key_file = key_file
346 self.key_file = key_file
347 self.cert_file = cert_file
347 self.cert_file = cert_file
348
348
349 def connect(self):
349 def connect(self):
350 self.sock = _create_connection((self.host, self.port))
350 self.sock = _create_connection((self.host, self.port))
351
351
352 host = self.host
352 host = self.host
353 if self.realhostport: # use CONNECT proxy
353 if self.realhostport: # use CONNECT proxy
354 _generic_proxytunnel(self)
354 _generic_proxytunnel(self)
355 host = self.realhostport.rsplit(':', 1)[0]
355 host = self.realhostport.rsplit(':', 1)[0]
356 self.sock = sslutil.wrapsocket(
356 self.sock = sslutil.wrapsocket(
357 self.sock, self.key_file, self.cert_file, serverhostname=host,
357 self.sock, self.key_file, self.cert_file, serverhostname=host,
358 **sslutil.sslkwargs(self.ui, host))
358 **sslutil.sslkwargs(self.ui, host))
359 sslutil.validator(self.ui, host)(self.sock)
359 sslutil.validatesocket(self.sock)
360
360
361 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
361 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
362 def __init__(self, ui):
362 def __init__(self, ui):
363 keepalive.KeepAliveHandler.__init__(self)
363 keepalive.KeepAliveHandler.__init__(self)
364 urlreq.httpshandler.__init__(self)
364 urlreq.httpshandler.__init__(self)
365 self.ui = ui
365 self.ui = ui
366 self.pwmgr = passwordmgr(self.ui)
366 self.pwmgr = passwordmgr(self.ui)
367
367
368 def _start_transaction(self, h, req):
368 def _start_transaction(self, h, req):
369 _generic_start_transaction(self, h, req)
369 _generic_start_transaction(self, h, req)
370 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
370 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
371
371
372 def https_open(self, req):
372 def https_open(self, req):
373 # req.get_full_url() does not contain credentials and we may
373 # req.get_full_url() does not contain credentials and we may
374 # need them to match the certificates.
374 # need them to match the certificates.
375 url = req.get_full_url()
375 url = req.get_full_url()
376 user, password = self.pwmgr.find_stored_password(url)
376 user, password = self.pwmgr.find_stored_password(url)
377 res = httpconnectionmod.readauthforuri(self.ui, url, user)
377 res = httpconnectionmod.readauthforuri(self.ui, url, user)
378 if res:
378 if res:
379 group, auth = res
379 group, auth = res
380 self.auth = auth
380 self.auth = auth
381 self.ui.debug("using auth.%s.* for authentication\n" % group)
381 self.ui.debug("using auth.%s.* for authentication\n" % group)
382 else:
382 else:
383 self.auth = None
383 self.auth = None
384 return self.do_open(self._makeconnection, req)
384 return self.do_open(self._makeconnection, req)
385
385
386 def _makeconnection(self, host, port=None, *args, **kwargs):
386 def _makeconnection(self, host, port=None, *args, **kwargs):
387 keyfile = None
387 keyfile = None
388 certfile = None
388 certfile = None
389
389
390 if len(args) >= 1: # key_file
390 if len(args) >= 1: # key_file
391 keyfile = args[0]
391 keyfile = args[0]
392 if len(args) >= 2: # cert_file
392 if len(args) >= 2: # cert_file
393 certfile = args[1]
393 certfile = args[1]
394 args = args[2:]
394 args = args[2:]
395
395
396 # if the user has specified different key/cert files in
396 # if the user has specified different key/cert files in
397 # hgrc, we prefer these
397 # hgrc, we prefer these
398 if self.auth and 'key' in self.auth and 'cert' in self.auth:
398 if self.auth and 'key' in self.auth and 'cert' in self.auth:
399 keyfile = self.auth['key']
399 keyfile = self.auth['key']
400 certfile = self.auth['cert']
400 certfile = self.auth['cert']
401
401
402 conn = httpsconnection(host, port, keyfile, certfile, *args,
402 conn = httpsconnection(host, port, keyfile, certfile, *args,
403 **kwargs)
403 **kwargs)
404 conn.ui = self.ui
404 conn.ui = self.ui
405 return conn
405 return conn
406
406
407 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
407 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
408 def __init__(self, *args, **kwargs):
408 def __init__(self, *args, **kwargs):
409 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
409 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
410 self.retried_req = None
410 self.retried_req = None
411
411
412 def reset_retry_count(self):
412 def reset_retry_count(self):
413 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
413 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
414 # forever. We disable reset_retry_count completely and reset in
414 # forever. We disable reset_retry_count completely and reset in
415 # http_error_auth_reqed instead.
415 # http_error_auth_reqed instead.
416 pass
416 pass
417
417
418 def http_error_auth_reqed(self, auth_header, host, req, headers):
418 def http_error_auth_reqed(self, auth_header, host, req, headers):
419 # Reset the retry counter once for each request.
419 # Reset the retry counter once for each request.
420 if req is not self.retried_req:
420 if req is not self.retried_req:
421 self.retried_req = req
421 self.retried_req = req
422 self.retried = 0
422 self.retried = 0
423 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
423 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
424 self, auth_header, host, req, headers)
424 self, auth_header, host, req, headers)
425
425
426 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
426 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
427 def __init__(self, *args, **kwargs):
427 def __init__(self, *args, **kwargs):
428 self.auth = None
428 self.auth = None
429 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
429 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
430 self.retried_req = None
430 self.retried_req = None
431
431
432 def http_request(self, request):
432 def http_request(self, request):
433 if self.auth:
433 if self.auth:
434 request.add_unredirected_header(self.auth_header, self.auth)
434 request.add_unredirected_header(self.auth_header, self.auth)
435
435
436 return request
436 return request
437
437
438 def https_request(self, request):
438 def https_request(self, request):
439 if self.auth:
439 if self.auth:
440 request.add_unredirected_header(self.auth_header, self.auth)
440 request.add_unredirected_header(self.auth_header, self.auth)
441
441
442 return request
442 return request
443
443
444 def reset_retry_count(self):
444 def reset_retry_count(self):
445 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
445 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
446 # forever. We disable reset_retry_count completely and reset in
446 # forever. We disable reset_retry_count completely and reset in
447 # http_error_auth_reqed instead.
447 # http_error_auth_reqed instead.
448 pass
448 pass
449
449
450 def http_error_auth_reqed(self, auth_header, host, req, headers):
450 def http_error_auth_reqed(self, auth_header, host, req, headers):
451 # Reset the retry counter once for each request.
451 # Reset the retry counter once for each request.
452 if req is not self.retried_req:
452 if req is not self.retried_req:
453 self.retried_req = req
453 self.retried_req = req
454 self.retried = 0
454 self.retried = 0
455 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
455 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
456 self, auth_header, host, req, headers)
456 self, auth_header, host, req, headers)
457
457
458 def retry_http_basic_auth(self, host, req, realm):
458 def retry_http_basic_auth(self, host, req, realm):
459 user, pw = self.passwd.find_user_password(realm, req.get_full_url())
459 user, pw = self.passwd.find_user_password(realm, req.get_full_url())
460 if pw is not None:
460 if pw is not None:
461 raw = "%s:%s" % (user, pw)
461 raw = "%s:%s" % (user, pw)
462 auth = 'Basic %s' % base64.b64encode(raw).strip()
462 auth = 'Basic %s' % base64.b64encode(raw).strip()
463 if req.headers.get(self.auth_header, None) == auth:
463 if req.headers.get(self.auth_header, None) == auth:
464 return None
464 return None
465 self.auth = auth
465 self.auth = auth
466 req.add_unredirected_header(self.auth_header, auth)
466 req.add_unredirected_header(self.auth_header, auth)
467 return self.parent.open(req)
467 return self.parent.open(req)
468 else:
468 else:
469 return None
469 return None
470
470
471 handlerfuncs = []
471 handlerfuncs = []
472
472
473 def opener(ui, authinfo=None):
473 def opener(ui, authinfo=None):
474 '''
474 '''
475 construct an opener suitable for urllib2
475 construct an opener suitable for urllib2
476 authinfo will be added to the password manager
476 authinfo will be added to the password manager
477 '''
477 '''
478 # experimental config: ui.usehttp2
478 # experimental config: ui.usehttp2
479 if ui.configbool('ui', 'usehttp2', False):
479 if ui.configbool('ui', 'usehttp2', False):
480 handlers = [httpconnectionmod.http2handler(ui, passwordmgr(ui))]
480 handlers = [httpconnectionmod.http2handler(ui, passwordmgr(ui))]
481 else:
481 else:
482 handlers = [httphandler()]
482 handlers = [httphandler()]
483 if has_https:
483 if has_https:
484 handlers.append(httpshandler(ui))
484 handlers.append(httpshandler(ui))
485
485
486 handlers.append(proxyhandler(ui))
486 handlers.append(proxyhandler(ui))
487
487
488 passmgr = passwordmgr(ui)
488 passmgr = passwordmgr(ui)
489 if authinfo is not None:
489 if authinfo is not None:
490 passmgr.add_password(*authinfo)
490 passmgr.add_password(*authinfo)
491 user, passwd = authinfo[2:4]
491 user, passwd = authinfo[2:4]
492 ui.debug('http auth: user %s, password %s\n' %
492 ui.debug('http auth: user %s, password %s\n' %
493 (user, passwd and '*' * len(passwd) or 'not set'))
493 (user, passwd and '*' * len(passwd) or 'not set'))
494
494
495 handlers.extend((httpbasicauthhandler(passmgr),
495 handlers.extend((httpbasicauthhandler(passmgr),
496 httpdigestauthhandler(passmgr)))
496 httpdigestauthhandler(passmgr)))
497 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
497 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
498 opener = urlreq.buildopener(*handlers)
498 opener = urlreq.buildopener(*handlers)
499
499
500 # 1.0 here is the _protocol_ version
500 # 1.0 here is the _protocol_ version
501 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
501 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
502 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
502 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
503 return opener
503 return opener
504
504
505 def open(ui, url_, data=None):
505 def open(ui, url_, data=None):
506 u = util.url(url_)
506 u = util.url(url_)
507 if u.scheme:
507 if u.scheme:
508 u.scheme = u.scheme.lower()
508 u.scheme = u.scheme.lower()
509 url_, authinfo = u.authinfo()
509 url_, authinfo = u.authinfo()
510 else:
510 else:
511 path = util.normpath(os.path.abspath(url_))
511 path = util.normpath(os.path.abspath(url_))
512 url_ = 'file://' + urlreq.pathname2url(path)
512 url_ = 'file://' + urlreq.pathname2url(path)
513 authinfo = None
513 authinfo = None
514 return opener(ui, authinfo).open(url_, data)
514 return opener(ui, authinfo).open(url_, data)
General Comments 0
You need to be logged in to leave comments. Login now