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