##// END OF EJS Templates
url: move _wraphttpresponse() from httpeer...
Gregory Szorc -
r40054:f80db6ad default
parent child Browse files
Show More
@@ -1,1006 +1,970 b''
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import io
12 import io
13 import os
13 import os
14 import socket
14 import socket
15 import struct
15 import struct
16 import weakref
16 import weakref
17
17
18 from .i18n import _
18 from .i18n import _
19 from . import (
19 from . import (
20 bundle2,
20 bundle2,
21 error,
21 error,
22 httpconnection,
22 httpconnection,
23 pycompat,
23 pycompat,
24 repository,
24 repository,
25 statichttprepo,
25 statichttprepo,
26 url as urlmod,
26 url as urlmod,
27 util,
27 util,
28 wireprotoframing,
28 wireprotoframing,
29 wireprototypes,
29 wireprototypes,
30 wireprotov1peer,
30 wireprotov1peer,
31 wireprotov2peer,
31 wireprotov2peer,
32 wireprotov2server,
32 wireprotov2server,
33 )
33 )
34 from .utils import (
34 from .utils import (
35 cborutil,
35 cborutil,
36 interfaceutil,
36 interfaceutil,
37 stringutil,
37 stringutil,
38 )
38 )
39
39
40 httplib = util.httplib
40 httplib = util.httplib
41 urlerr = util.urlerr
41 urlerr = util.urlerr
42 urlreq = util.urlreq
42 urlreq = util.urlreq
43
43
44 def encodevalueinheaders(value, header, limit):
44 def encodevalueinheaders(value, header, limit):
45 """Encode a string value into multiple HTTP headers.
45 """Encode a string value into multiple HTTP headers.
46
46
47 ``value`` will be encoded into 1 or more HTTP headers with the names
47 ``value`` will be encoded into 1 or more HTTP headers with the names
48 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
48 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
49 name + value will be at most ``limit`` bytes long.
49 name + value will be at most ``limit`` bytes long.
50
50
51 Returns an iterable of 2-tuples consisting of header names and
51 Returns an iterable of 2-tuples consisting of header names and
52 values as native strings.
52 values as native strings.
53 """
53 """
54 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
54 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
55 # not bytes. This function always takes bytes in as arguments.
55 # not bytes. This function always takes bytes in as arguments.
56 fmt = pycompat.strurl(header) + r'-%s'
56 fmt = pycompat.strurl(header) + r'-%s'
57 # Note: it is *NOT* a bug that the last bit here is a bytestring
57 # Note: it is *NOT* a bug that the last bit here is a bytestring
58 # and not a unicode: we're just getting the encoded length anyway,
58 # and not a unicode: we're just getting the encoded length anyway,
59 # and using an r-string to make it portable between Python 2 and 3
59 # and using an r-string to make it portable between Python 2 and 3
60 # doesn't work because then the \r is a literal backslash-r
60 # doesn't work because then the \r is a literal backslash-r
61 # instead of a carriage return.
61 # instead of a carriage return.
62 valuelen = limit - len(fmt % r'000') - len(': \r\n')
62 valuelen = limit - len(fmt % r'000') - len(': \r\n')
63 result = []
63 result = []
64
64
65 n = 0
65 n = 0
66 for i in pycompat.xrange(0, len(value), valuelen):
66 for i in pycompat.xrange(0, len(value), valuelen):
67 n += 1
67 n += 1
68 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
68 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
69
69
70 return result
70 return result
71
71
72 def _wraphttpresponse(resp):
73 """Wrap an HTTPResponse with common error handlers.
74
75 This ensures that any I/O from any consumer raises the appropriate
76 error and messaging.
77 """
78 origread = resp.read
79
80 class readerproxy(resp.__class__):
81 def read(self, size=None):
82 try:
83 return origread(size)
84 except httplib.IncompleteRead as e:
85 # e.expected is an integer if length known or None otherwise.
86 if e.expected:
87 got = len(e.partial)
88 total = e.expected + got
89 msg = _('HTTP request error (incomplete response; '
90 'expected %d bytes got %d)') % (total, got)
91 else:
92 msg = _('HTTP request error (incomplete response)')
93
94 raise error.PeerTransportError(
95 msg,
96 hint=_('this may be an intermittent network failure; '
97 'if the error persists, consider contacting the '
98 'network or server operator'))
99 except httplib.HTTPException as e:
100 raise error.PeerTransportError(
101 _('HTTP request error (%s)') % e,
102 hint=_('this may be an intermittent network failure; '
103 'if the error persists, consider contacting the '
104 'network or server operator'))
105
106 resp.__class__ = readerproxy
107
108 class _multifile(object):
72 class _multifile(object):
109 def __init__(self, *fileobjs):
73 def __init__(self, *fileobjs):
110 for f in fileobjs:
74 for f in fileobjs:
111 if not util.safehasattr(f, 'length'):
75 if not util.safehasattr(f, 'length'):
112 raise ValueError(
76 raise ValueError(
113 '_multifile only supports file objects that '
77 '_multifile only supports file objects that '
114 'have a length but this one does not:', type(f), f)
78 'have a length but this one does not:', type(f), f)
115 self._fileobjs = fileobjs
79 self._fileobjs = fileobjs
116 self._index = 0
80 self._index = 0
117
81
118 @property
82 @property
119 def length(self):
83 def length(self):
120 return sum(f.length for f in self._fileobjs)
84 return sum(f.length for f in self._fileobjs)
121
85
122 def read(self, amt=None):
86 def read(self, amt=None):
123 if amt <= 0:
87 if amt <= 0:
124 return ''.join(f.read() for f in self._fileobjs)
88 return ''.join(f.read() for f in self._fileobjs)
125 parts = []
89 parts = []
126 while amt and self._index < len(self._fileobjs):
90 while amt and self._index < len(self._fileobjs):
127 parts.append(self._fileobjs[self._index].read(amt))
91 parts.append(self._fileobjs[self._index].read(amt))
128 got = len(parts[-1])
92 got = len(parts[-1])
129 if got < amt:
93 if got < amt:
130 self._index += 1
94 self._index += 1
131 amt -= got
95 amt -= got
132 return ''.join(parts)
96 return ''.join(parts)
133
97
134 def seek(self, offset, whence=os.SEEK_SET):
98 def seek(self, offset, whence=os.SEEK_SET):
135 if whence != os.SEEK_SET:
99 if whence != os.SEEK_SET:
136 raise NotImplementedError(
100 raise NotImplementedError(
137 '_multifile does not support anything other'
101 '_multifile does not support anything other'
138 ' than os.SEEK_SET for whence on seek()')
102 ' than os.SEEK_SET for whence on seek()')
139 if offset != 0:
103 if offset != 0:
140 raise NotImplementedError(
104 raise NotImplementedError(
141 '_multifile only supports seeking to start, but that '
105 '_multifile only supports seeking to start, but that '
142 'could be fixed if you need it')
106 'could be fixed if you need it')
143 for f in self._fileobjs:
107 for f in self._fileobjs:
144 f.seek(0)
108 f.seek(0)
145 self._index = 0
109 self._index = 0
146
110
147 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
111 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
148 repobaseurl, cmd, args):
112 repobaseurl, cmd, args):
149 """Make an HTTP request to run a command for a version 1 client.
113 """Make an HTTP request to run a command for a version 1 client.
150
114
151 ``caps`` is a set of known server capabilities. The value may be
115 ``caps`` is a set of known server capabilities. The value may be
152 None if capabilities are not yet known.
116 None if capabilities are not yet known.
153
117
154 ``capablefn`` is a function to evaluate a capability.
118 ``capablefn`` is a function to evaluate a capability.
155
119
156 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
120 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
157 raw data to pass to it.
121 raw data to pass to it.
158 """
122 """
159 if cmd == 'pushkey':
123 if cmd == 'pushkey':
160 args['data'] = ''
124 args['data'] = ''
161 data = args.pop('data', None)
125 data = args.pop('data', None)
162 headers = args.pop('headers', {})
126 headers = args.pop('headers', {})
163
127
164 ui.debug("sending %s command\n" % cmd)
128 ui.debug("sending %s command\n" % cmd)
165 q = [('cmd', cmd)]
129 q = [('cmd', cmd)]
166 headersize = 0
130 headersize = 0
167 # Important: don't use self.capable() here or else you end up
131 # Important: don't use self.capable() here or else you end up
168 # with infinite recursion when trying to look up capabilities
132 # with infinite recursion when trying to look up capabilities
169 # for the first time.
133 # for the first time.
170 postargsok = caps is not None and 'httppostargs' in caps
134 postargsok = caps is not None and 'httppostargs' in caps
171
135
172 # Send arguments via POST.
136 # Send arguments via POST.
173 if postargsok and args:
137 if postargsok and args:
174 strargs = urlreq.urlencode(sorted(args.items()))
138 strargs = urlreq.urlencode(sorted(args.items()))
175 if not data:
139 if not data:
176 data = strargs
140 data = strargs
177 else:
141 else:
178 if isinstance(data, bytes):
142 if isinstance(data, bytes):
179 i = io.BytesIO(data)
143 i = io.BytesIO(data)
180 i.length = len(data)
144 i.length = len(data)
181 data = i
145 data = i
182 argsio = io.BytesIO(strargs)
146 argsio = io.BytesIO(strargs)
183 argsio.length = len(strargs)
147 argsio.length = len(strargs)
184 data = _multifile(argsio, data)
148 data = _multifile(argsio, data)
185 headers[r'X-HgArgs-Post'] = len(strargs)
149 headers[r'X-HgArgs-Post'] = len(strargs)
186 elif args:
150 elif args:
187 # Calling self.capable() can infinite loop if we are calling
151 # Calling self.capable() can infinite loop if we are calling
188 # "capabilities". But that command should never accept wire
152 # "capabilities". But that command should never accept wire
189 # protocol arguments. So this should never happen.
153 # protocol arguments. So this should never happen.
190 assert cmd != 'capabilities'
154 assert cmd != 'capabilities'
191 httpheader = capablefn('httpheader')
155 httpheader = capablefn('httpheader')
192 if httpheader:
156 if httpheader:
193 headersize = int(httpheader.split(',', 1)[0])
157 headersize = int(httpheader.split(',', 1)[0])
194
158
195 # Send arguments via HTTP headers.
159 # Send arguments via HTTP headers.
196 if headersize > 0:
160 if headersize > 0:
197 # The headers can typically carry more data than the URL.
161 # The headers can typically carry more data than the URL.
198 encargs = urlreq.urlencode(sorted(args.items()))
162 encargs = urlreq.urlencode(sorted(args.items()))
199 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
163 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
200 headersize):
164 headersize):
201 headers[header] = value
165 headers[header] = value
202 # Send arguments via query string (Mercurial <1.9).
166 # Send arguments via query string (Mercurial <1.9).
203 else:
167 else:
204 q += sorted(args.items())
168 q += sorted(args.items())
205
169
206 qs = '?%s' % urlreq.urlencode(q)
170 qs = '?%s' % urlreq.urlencode(q)
207 cu = "%s%s" % (repobaseurl, qs)
171 cu = "%s%s" % (repobaseurl, qs)
208 size = 0
172 size = 0
209 if util.safehasattr(data, 'length'):
173 if util.safehasattr(data, 'length'):
210 size = data.length
174 size = data.length
211 elif data is not None:
175 elif data is not None:
212 size = len(data)
176 size = len(data)
213 if data is not None and r'Content-Type' not in headers:
177 if data is not None and r'Content-Type' not in headers:
214 headers[r'Content-Type'] = r'application/mercurial-0.1'
178 headers[r'Content-Type'] = r'application/mercurial-0.1'
215
179
216 # Tell the server we accept application/mercurial-0.2 and multiple
180 # Tell the server we accept application/mercurial-0.2 and multiple
217 # compression formats if the server is capable of emitting those
181 # compression formats if the server is capable of emitting those
218 # payloads.
182 # payloads.
219 # Note: Keep this set empty by default, as client advertisement of
183 # Note: Keep this set empty by default, as client advertisement of
220 # protocol parameters should only occur after the handshake.
184 # protocol parameters should only occur after the handshake.
221 protoparams = set()
185 protoparams = set()
222
186
223 mediatypes = set()
187 mediatypes = set()
224 if caps is not None:
188 if caps is not None:
225 mt = capablefn('httpmediatype')
189 mt = capablefn('httpmediatype')
226 if mt:
190 if mt:
227 protoparams.add('0.1')
191 protoparams.add('0.1')
228 mediatypes = set(mt.split(','))
192 mediatypes = set(mt.split(','))
229
193
230 protoparams.add('partial-pull')
194 protoparams.add('partial-pull')
231
195
232 if '0.2tx' in mediatypes:
196 if '0.2tx' in mediatypes:
233 protoparams.add('0.2')
197 protoparams.add('0.2')
234
198
235 if '0.2tx' in mediatypes and capablefn('compression'):
199 if '0.2tx' in mediatypes and capablefn('compression'):
236 # We /could/ compare supported compression formats and prune
200 # We /could/ compare supported compression formats and prune
237 # non-mutually supported or error if nothing is mutually supported.
201 # non-mutually supported or error if nothing is mutually supported.
238 # For now, send the full list to the server and have it error.
202 # For now, send the full list to the server and have it error.
239 comps = [e.wireprotosupport().name for e in
203 comps = [e.wireprotosupport().name for e in
240 util.compengines.supportedwireengines(util.CLIENTROLE)]
204 util.compengines.supportedwireengines(util.CLIENTROLE)]
241 protoparams.add('comp=%s' % ','.join(comps))
205 protoparams.add('comp=%s' % ','.join(comps))
242
206
243 if protoparams:
207 if protoparams:
244 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
208 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
245 'X-HgProto',
209 'X-HgProto',
246 headersize or 1024)
210 headersize or 1024)
247 for header, value in protoheaders:
211 for header, value in protoheaders:
248 headers[header] = value
212 headers[header] = value
249
213
250 varyheaders = []
214 varyheaders = []
251 for header in headers:
215 for header in headers:
252 if header.lower().startswith(r'x-hg'):
216 if header.lower().startswith(r'x-hg'):
253 varyheaders.append(header)
217 varyheaders.append(header)
254
218
255 if varyheaders:
219 if varyheaders:
256 headers[r'Vary'] = r','.join(sorted(varyheaders))
220 headers[r'Vary'] = r','.join(sorted(varyheaders))
257
221
258 req = requestbuilder(pycompat.strurl(cu), data, headers)
222 req = requestbuilder(pycompat.strurl(cu), data, headers)
259
223
260 if data is not None:
224 if data is not None:
261 ui.debug("sending %d bytes\n" % size)
225 ui.debug("sending %d bytes\n" % size)
262 req.add_unredirected_header(r'Content-Length', r'%d' % size)
226 req.add_unredirected_header(r'Content-Length', r'%d' % size)
263
227
264 return req, cu, qs
228 return req, cu, qs
265
229
266 def _reqdata(req):
230 def _reqdata(req):
267 """Get request data, if any. If no data, returns None."""
231 """Get request data, if any. If no data, returns None."""
268 if pycompat.ispy3:
232 if pycompat.ispy3:
269 return req.data
233 return req.data
270 if not req.has_data():
234 if not req.has_data():
271 return None
235 return None
272 return req.get_data()
236 return req.get_data()
273
237
274 def sendrequest(ui, opener, req):
238 def sendrequest(ui, opener, req):
275 """Send a prepared HTTP request.
239 """Send a prepared HTTP request.
276
240
277 Returns the response object.
241 Returns the response object.
278 """
242 """
279 dbg = ui.debug
243 dbg = ui.debug
280 if (ui.debugflag
244 if (ui.debugflag
281 and ui.configbool('devel', 'debug.peer-request')):
245 and ui.configbool('devel', 'debug.peer-request')):
282 line = 'devel-peer-request: %s\n'
246 line = 'devel-peer-request: %s\n'
283 dbg(line % '%s %s' % (pycompat.bytesurl(req.get_method()),
247 dbg(line % '%s %s' % (pycompat.bytesurl(req.get_method()),
284 pycompat.bytesurl(req.get_full_url())))
248 pycompat.bytesurl(req.get_full_url())))
285 hgargssize = None
249 hgargssize = None
286
250
287 for header, value in sorted(req.header_items()):
251 for header, value in sorted(req.header_items()):
288 header = pycompat.bytesurl(header)
252 header = pycompat.bytesurl(header)
289 value = pycompat.bytesurl(value)
253 value = pycompat.bytesurl(value)
290 if header.startswith('X-hgarg-'):
254 if header.startswith('X-hgarg-'):
291 if hgargssize is None:
255 if hgargssize is None:
292 hgargssize = 0
256 hgargssize = 0
293 hgargssize += len(value)
257 hgargssize += len(value)
294 else:
258 else:
295 dbg(line % ' %s %s' % (header, value))
259 dbg(line % ' %s %s' % (header, value))
296
260
297 if hgargssize is not None:
261 if hgargssize is not None:
298 dbg(line % ' %d bytes of commands arguments in headers'
262 dbg(line % ' %d bytes of commands arguments in headers'
299 % hgargssize)
263 % hgargssize)
300 data = _reqdata(req)
264 data = _reqdata(req)
301 if data is not None:
265 if data is not None:
302 length = getattr(data, 'length', None)
266 length = getattr(data, 'length', None)
303 if length is None:
267 if length is None:
304 length = len(data)
268 length = len(data)
305 dbg(line % ' %d bytes of data' % length)
269 dbg(line % ' %d bytes of data' % length)
306
270
307 start = util.timer()
271 start = util.timer()
308
272
309 res = None
273 res = None
310 try:
274 try:
311 res = opener.open(req)
275 res = opener.open(req)
312 except urlerr.httperror as inst:
276 except urlerr.httperror as inst:
313 if inst.code == 401:
277 if inst.code == 401:
314 raise error.Abort(_('authorization failed'))
278 raise error.Abort(_('authorization failed'))
315 raise
279 raise
316 except httplib.HTTPException as inst:
280 except httplib.HTTPException as inst:
317 ui.debug('http error requesting %s\n' %
281 ui.debug('http error requesting %s\n' %
318 util.hidepassword(req.get_full_url()))
282 util.hidepassword(req.get_full_url()))
319 ui.traceback()
283 ui.traceback()
320 raise IOError(None, inst)
284 raise IOError(None, inst)
321 finally:
285 finally:
322 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
286 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
323 code = res.code if res else -1
287 code = res.code if res else -1
324 dbg(line % ' finished in %.4f seconds (%d)'
288 dbg(line % ' finished in %.4f seconds (%d)'
325 % (util.timer() - start, code))
289 % (util.timer() - start, code))
326
290
327 # Insert error handlers for common I/O failures.
291 # Insert error handlers for common I/O failures.
328 _wraphttpresponse(res)
292 urlmod.wrapresponse(res)
329
293
330 return res
294 return res
331
295
332 class RedirectedRepoError(error.RepoError):
296 class RedirectedRepoError(error.RepoError):
333 def __init__(self, msg, respurl):
297 def __init__(self, msg, respurl):
334 super(RedirectedRepoError, self).__init__(msg)
298 super(RedirectedRepoError, self).__init__(msg)
335 self.respurl = respurl
299 self.respurl = respurl
336
300
337 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
301 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
338 allowcbor=False):
302 allowcbor=False):
339 # record the url we got redirected to
303 # record the url we got redirected to
340 redirected = False
304 redirected = False
341 respurl = pycompat.bytesurl(resp.geturl())
305 respurl = pycompat.bytesurl(resp.geturl())
342 if respurl.endswith(qs):
306 if respurl.endswith(qs):
343 respurl = respurl[:-len(qs)]
307 respurl = respurl[:-len(qs)]
344 qsdropped = False
308 qsdropped = False
345 else:
309 else:
346 qsdropped = True
310 qsdropped = True
347
311
348 if baseurl.rstrip('/') != respurl.rstrip('/'):
312 if baseurl.rstrip('/') != respurl.rstrip('/'):
349 redirected = True
313 redirected = True
350 if not ui.quiet:
314 if not ui.quiet:
351 ui.warn(_('real URL is %s\n') % respurl)
315 ui.warn(_('real URL is %s\n') % respurl)
352
316
353 try:
317 try:
354 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
318 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
355 except AttributeError:
319 except AttributeError:
356 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
320 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
357
321
358 safeurl = util.hidepassword(baseurl)
322 safeurl = util.hidepassword(baseurl)
359 if proto.startswith('application/hg-error'):
323 if proto.startswith('application/hg-error'):
360 raise error.OutOfBandError(resp.read())
324 raise error.OutOfBandError(resp.read())
361
325
362 # Pre 1.0 versions of Mercurial used text/plain and
326 # Pre 1.0 versions of Mercurial used text/plain and
363 # application/hg-changegroup. We don't support such old servers.
327 # application/hg-changegroup. We don't support such old servers.
364 if not proto.startswith('application/mercurial-'):
328 if not proto.startswith('application/mercurial-'):
365 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
329 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
366 msg = _("'%s' does not appear to be an hg repository:\n"
330 msg = _("'%s' does not appear to be an hg repository:\n"
367 "---%%<--- (%s)\n%s\n---%%<---\n") % (
331 "---%%<--- (%s)\n%s\n---%%<---\n") % (
368 safeurl, proto or 'no content-type', resp.read(1024))
332 safeurl, proto or 'no content-type', resp.read(1024))
369
333
370 # Some servers may strip the query string from the redirect. We
334 # Some servers may strip the query string from the redirect. We
371 # raise a special error type so callers can react to this specially.
335 # raise a special error type so callers can react to this specially.
372 if redirected and qsdropped:
336 if redirected and qsdropped:
373 raise RedirectedRepoError(msg, respurl)
337 raise RedirectedRepoError(msg, respurl)
374 else:
338 else:
375 raise error.RepoError(msg)
339 raise error.RepoError(msg)
376
340
377 try:
341 try:
378 subtype = proto.split('-', 1)[1]
342 subtype = proto.split('-', 1)[1]
379
343
380 # Unless we end up supporting CBOR in the legacy wire protocol,
344 # Unless we end up supporting CBOR in the legacy wire protocol,
381 # this should ONLY be encountered for the initial capabilities
345 # this should ONLY be encountered for the initial capabilities
382 # request during handshake.
346 # request during handshake.
383 if subtype == 'cbor':
347 if subtype == 'cbor':
384 if allowcbor:
348 if allowcbor:
385 return respurl, proto, resp
349 return respurl, proto, resp
386 else:
350 else:
387 raise error.RepoError(_('unexpected CBOR response from '
351 raise error.RepoError(_('unexpected CBOR response from '
388 'server'))
352 'server'))
389
353
390 version_info = tuple([int(n) for n in subtype.split('.')])
354 version_info = tuple([int(n) for n in subtype.split('.')])
391 except ValueError:
355 except ValueError:
392 raise error.RepoError(_("'%s' sent a broken Content-Type "
356 raise error.RepoError(_("'%s' sent a broken Content-Type "
393 "header (%s)") % (safeurl, proto))
357 "header (%s)") % (safeurl, proto))
394
358
395 # TODO consider switching to a decompression reader that uses
359 # TODO consider switching to a decompression reader that uses
396 # generators.
360 # generators.
397 if version_info == (0, 1):
361 if version_info == (0, 1):
398 if compressible:
362 if compressible:
399 resp = util.compengines['zlib'].decompressorreader(resp)
363 resp = util.compengines['zlib'].decompressorreader(resp)
400
364
401 elif version_info == (0, 2):
365 elif version_info == (0, 2):
402 # application/mercurial-0.2 always identifies the compression
366 # application/mercurial-0.2 always identifies the compression
403 # engine in the payload header.
367 # engine in the payload header.
404 elen = struct.unpack('B', util.readexactly(resp, 1))[0]
368 elen = struct.unpack('B', util.readexactly(resp, 1))[0]
405 ename = util.readexactly(resp, elen)
369 ename = util.readexactly(resp, elen)
406 engine = util.compengines.forwiretype(ename)
370 engine = util.compengines.forwiretype(ename)
407
371
408 resp = engine.decompressorreader(resp)
372 resp = engine.decompressorreader(resp)
409 else:
373 else:
410 raise error.RepoError(_("'%s' uses newer protocol %s") %
374 raise error.RepoError(_("'%s' uses newer protocol %s") %
411 (safeurl, subtype))
375 (safeurl, subtype))
412
376
413 return respurl, proto, resp
377 return respurl, proto, resp
414
378
415 class httppeer(wireprotov1peer.wirepeer):
379 class httppeer(wireprotov1peer.wirepeer):
416 def __init__(self, ui, path, url, opener, requestbuilder, caps):
380 def __init__(self, ui, path, url, opener, requestbuilder, caps):
417 self.ui = ui
381 self.ui = ui
418 self._path = path
382 self._path = path
419 self._url = url
383 self._url = url
420 self._caps = caps
384 self._caps = caps
421 self._urlopener = opener
385 self._urlopener = opener
422 self._requestbuilder = requestbuilder
386 self._requestbuilder = requestbuilder
423
387
424 def __del__(self):
388 def __del__(self):
425 for h in self._urlopener.handlers:
389 for h in self._urlopener.handlers:
426 h.close()
390 h.close()
427 getattr(h, "close_all", lambda: None)()
391 getattr(h, "close_all", lambda: None)()
428
392
429 # Begin of ipeerconnection interface.
393 # Begin of ipeerconnection interface.
430
394
431 def url(self):
395 def url(self):
432 return self._path
396 return self._path
433
397
434 def local(self):
398 def local(self):
435 return None
399 return None
436
400
437 def peer(self):
401 def peer(self):
438 return self
402 return self
439
403
440 def canpush(self):
404 def canpush(self):
441 return True
405 return True
442
406
443 def close(self):
407 def close(self):
444 pass
408 pass
445
409
446 # End of ipeerconnection interface.
410 # End of ipeerconnection interface.
447
411
448 # Begin of ipeercommands interface.
412 # Begin of ipeercommands interface.
449
413
450 def capabilities(self):
414 def capabilities(self):
451 return self._caps
415 return self._caps
452
416
453 # End of ipeercommands interface.
417 # End of ipeercommands interface.
454
418
455 def _callstream(self, cmd, _compressible=False, **args):
419 def _callstream(self, cmd, _compressible=False, **args):
456 args = pycompat.byteskwargs(args)
420 args = pycompat.byteskwargs(args)
457
421
458 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
422 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
459 self._caps, self.capable,
423 self._caps, self.capable,
460 self._url, cmd, args)
424 self._url, cmd, args)
461
425
462 resp = sendrequest(self.ui, self._urlopener, req)
426 resp = sendrequest(self.ui, self._urlopener, req)
463
427
464 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
428 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
465 resp, _compressible)
429 resp, _compressible)
466
430
467 return resp
431 return resp
468
432
469 def _call(self, cmd, **args):
433 def _call(self, cmd, **args):
470 fp = self._callstream(cmd, **args)
434 fp = self._callstream(cmd, **args)
471 try:
435 try:
472 return fp.read()
436 return fp.read()
473 finally:
437 finally:
474 # if using keepalive, allow connection to be reused
438 # if using keepalive, allow connection to be reused
475 fp.close()
439 fp.close()
476
440
477 def _callpush(self, cmd, cg, **args):
441 def _callpush(self, cmd, cg, **args):
478 # have to stream bundle to a temp file because we do not have
442 # have to stream bundle to a temp file because we do not have
479 # http 1.1 chunked transfer.
443 # http 1.1 chunked transfer.
480
444
481 types = self.capable('unbundle')
445 types = self.capable('unbundle')
482 try:
446 try:
483 types = types.split(',')
447 types = types.split(',')
484 except AttributeError:
448 except AttributeError:
485 # servers older than d1b16a746db6 will send 'unbundle' as a
449 # servers older than d1b16a746db6 will send 'unbundle' as a
486 # boolean capability. They only support headerless/uncompressed
450 # boolean capability. They only support headerless/uncompressed
487 # bundles.
451 # bundles.
488 types = [""]
452 types = [""]
489 for x in types:
453 for x in types:
490 if x in bundle2.bundletypes:
454 if x in bundle2.bundletypes:
491 type = x
455 type = x
492 break
456 break
493
457
494 tempname = bundle2.writebundle(self.ui, cg, None, type)
458 tempname = bundle2.writebundle(self.ui, cg, None, type)
495 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
459 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
496 headers = {r'Content-Type': r'application/mercurial-0.1'}
460 headers = {r'Content-Type': r'application/mercurial-0.1'}
497
461
498 try:
462 try:
499 r = self._call(cmd, data=fp, headers=headers, **args)
463 r = self._call(cmd, data=fp, headers=headers, **args)
500 vals = r.split('\n', 1)
464 vals = r.split('\n', 1)
501 if len(vals) < 2:
465 if len(vals) < 2:
502 raise error.ResponseError(_("unexpected response:"), r)
466 raise error.ResponseError(_("unexpected response:"), r)
503 return vals
467 return vals
504 except urlerr.httperror:
468 except urlerr.httperror:
505 # Catch and re-raise these so we don't try and treat them
469 # Catch and re-raise these so we don't try and treat them
506 # like generic socket errors. They lack any values in
470 # like generic socket errors. They lack any values in
507 # .args on Python 3 which breaks our socket.error block.
471 # .args on Python 3 which breaks our socket.error block.
508 raise
472 raise
509 except socket.error as err:
473 except socket.error as err:
510 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
474 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
511 raise error.Abort(_('push failed: %s') % err.args[1])
475 raise error.Abort(_('push failed: %s') % err.args[1])
512 raise error.Abort(err.args[1])
476 raise error.Abort(err.args[1])
513 finally:
477 finally:
514 fp.close()
478 fp.close()
515 os.unlink(tempname)
479 os.unlink(tempname)
516
480
517 def _calltwowaystream(self, cmd, fp, **args):
481 def _calltwowaystream(self, cmd, fp, **args):
518 fh = None
482 fh = None
519 fp_ = None
483 fp_ = None
520 filename = None
484 filename = None
521 try:
485 try:
522 # dump bundle to disk
486 # dump bundle to disk
523 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
487 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
524 fh = os.fdopen(fd, r"wb")
488 fh = os.fdopen(fd, r"wb")
525 d = fp.read(4096)
489 d = fp.read(4096)
526 while d:
490 while d:
527 fh.write(d)
491 fh.write(d)
528 d = fp.read(4096)
492 d = fp.read(4096)
529 fh.close()
493 fh.close()
530 # start http push
494 # start http push
531 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
495 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
532 headers = {r'Content-Type': r'application/mercurial-0.1'}
496 headers = {r'Content-Type': r'application/mercurial-0.1'}
533 return self._callstream(cmd, data=fp_, headers=headers, **args)
497 return self._callstream(cmd, data=fp_, headers=headers, **args)
534 finally:
498 finally:
535 if fp_ is not None:
499 if fp_ is not None:
536 fp_.close()
500 fp_.close()
537 if fh is not None:
501 if fh is not None:
538 fh.close()
502 fh.close()
539 os.unlink(filename)
503 os.unlink(filename)
540
504
541 def _callcompressable(self, cmd, **args):
505 def _callcompressable(self, cmd, **args):
542 return self._callstream(cmd, _compressible=True, **args)
506 return self._callstream(cmd, _compressible=True, **args)
543
507
544 def _abort(self, exception):
508 def _abort(self, exception):
545 raise exception
509 raise exception
546
510
547 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests):
511 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests):
548 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
512 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
549 buffersends=True)
513 buffersends=True)
550
514
551 handler = wireprotov2peer.clienthandler(ui, reactor)
515 handler = wireprotov2peer.clienthandler(ui, reactor)
552
516
553 url = '%s/%s' % (apiurl, permission)
517 url = '%s/%s' % (apiurl, permission)
554
518
555 if len(requests) > 1:
519 if len(requests) > 1:
556 url += '/multirequest'
520 url += '/multirequest'
557 else:
521 else:
558 url += '/%s' % requests[0][0]
522 url += '/%s' % requests[0][0]
559
523
560 ui.debug('sending %d commands\n' % len(requests))
524 ui.debug('sending %d commands\n' % len(requests))
561 for command, args, f in requests:
525 for command, args, f in requests:
562 ui.debug('sending command %s: %s\n' % (
526 ui.debug('sending command %s: %s\n' % (
563 command, stringutil.pprint(args, indent=2)))
527 command, stringutil.pprint(args, indent=2)))
564 assert not list(handler.callcommand(command, args, f))
528 assert not list(handler.callcommand(command, args, f))
565
529
566 # TODO stream this.
530 # TODO stream this.
567 body = b''.join(map(bytes, handler.flushcommands()))
531 body = b''.join(map(bytes, handler.flushcommands()))
568
532
569 # TODO modify user-agent to reflect v2
533 # TODO modify user-agent to reflect v2
570 headers = {
534 headers = {
571 r'Accept': wireprotov2server.FRAMINGTYPE,
535 r'Accept': wireprotov2server.FRAMINGTYPE,
572 r'Content-Type': wireprotov2server.FRAMINGTYPE,
536 r'Content-Type': wireprotov2server.FRAMINGTYPE,
573 }
537 }
574
538
575 req = requestbuilder(pycompat.strurl(url), body, headers)
539 req = requestbuilder(pycompat.strurl(url), body, headers)
576 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
540 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
577
541
578 try:
542 try:
579 res = opener.open(req)
543 res = opener.open(req)
580 except urlerr.httperror as e:
544 except urlerr.httperror as e:
581 if e.code == 401:
545 if e.code == 401:
582 raise error.Abort(_('authorization failed'))
546 raise error.Abort(_('authorization failed'))
583
547
584 raise
548 raise
585 except httplib.HTTPException as e:
549 except httplib.HTTPException as e:
586 ui.traceback()
550 ui.traceback()
587 raise IOError(None, e)
551 raise IOError(None, e)
588
552
589 return handler, res
553 return handler, res
590
554
591 class queuedcommandfuture(pycompat.futures.Future):
555 class queuedcommandfuture(pycompat.futures.Future):
592 """Wraps result() on command futures to trigger submission on call."""
556 """Wraps result() on command futures to trigger submission on call."""
593
557
594 def result(self, timeout=None):
558 def result(self, timeout=None):
595 if self.done():
559 if self.done():
596 return pycompat.futures.Future.result(self, timeout)
560 return pycompat.futures.Future.result(self, timeout)
597
561
598 self._peerexecutor.sendcommands()
562 self._peerexecutor.sendcommands()
599
563
600 # sendcommands() will restore the original __class__ and self.result
564 # sendcommands() will restore the original __class__ and self.result
601 # will resolve to Future.result.
565 # will resolve to Future.result.
602 return self.result(timeout)
566 return self.result(timeout)
603
567
604 @interfaceutil.implementer(repository.ipeercommandexecutor)
568 @interfaceutil.implementer(repository.ipeercommandexecutor)
605 class httpv2executor(object):
569 class httpv2executor(object):
606 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor):
570 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor):
607 self._ui = ui
571 self._ui = ui
608 self._opener = opener
572 self._opener = opener
609 self._requestbuilder = requestbuilder
573 self._requestbuilder = requestbuilder
610 self._apiurl = apiurl
574 self._apiurl = apiurl
611 self._descriptor = descriptor
575 self._descriptor = descriptor
612 self._sent = False
576 self._sent = False
613 self._closed = False
577 self._closed = False
614 self._neededpermissions = set()
578 self._neededpermissions = set()
615 self._calls = []
579 self._calls = []
616 self._futures = weakref.WeakSet()
580 self._futures = weakref.WeakSet()
617 self._responseexecutor = None
581 self._responseexecutor = None
618 self._responsef = None
582 self._responsef = None
619
583
620 def __enter__(self):
584 def __enter__(self):
621 return self
585 return self
622
586
623 def __exit__(self, exctype, excvalue, exctb):
587 def __exit__(self, exctype, excvalue, exctb):
624 self.close()
588 self.close()
625
589
626 def callcommand(self, command, args):
590 def callcommand(self, command, args):
627 if self._sent:
591 if self._sent:
628 raise error.ProgrammingError('callcommand() cannot be used after '
592 raise error.ProgrammingError('callcommand() cannot be used after '
629 'commands are sent')
593 'commands are sent')
630
594
631 if self._closed:
595 if self._closed:
632 raise error.ProgrammingError('callcommand() cannot be used after '
596 raise error.ProgrammingError('callcommand() cannot be used after '
633 'close()')
597 'close()')
634
598
635 # The service advertises which commands are available. So if we attempt
599 # The service advertises which commands are available. So if we attempt
636 # to call an unknown command or pass an unknown argument, we can screen
600 # to call an unknown command or pass an unknown argument, we can screen
637 # for this.
601 # for this.
638 if command not in self._descriptor['commands']:
602 if command not in self._descriptor['commands']:
639 raise error.ProgrammingError(
603 raise error.ProgrammingError(
640 'wire protocol command %s is not available' % command)
604 'wire protocol command %s is not available' % command)
641
605
642 cmdinfo = self._descriptor['commands'][command]
606 cmdinfo = self._descriptor['commands'][command]
643 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
607 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
644
608
645 if unknownargs:
609 if unknownargs:
646 raise error.ProgrammingError(
610 raise error.ProgrammingError(
647 'wire protocol command %s does not accept argument: %s' % (
611 'wire protocol command %s does not accept argument: %s' % (
648 command, ', '.join(sorted(unknownargs))))
612 command, ', '.join(sorted(unknownargs))))
649
613
650 self._neededpermissions |= set(cmdinfo['permissions'])
614 self._neededpermissions |= set(cmdinfo['permissions'])
651
615
652 # TODO we /could/ also validate types here, since the API descriptor
616 # TODO we /could/ also validate types here, since the API descriptor
653 # includes types...
617 # includes types...
654
618
655 f = pycompat.futures.Future()
619 f = pycompat.futures.Future()
656
620
657 # Monkeypatch it so result() triggers sendcommands(), otherwise result()
621 # Monkeypatch it so result() triggers sendcommands(), otherwise result()
658 # could deadlock.
622 # could deadlock.
659 f.__class__ = queuedcommandfuture
623 f.__class__ = queuedcommandfuture
660 f._peerexecutor = self
624 f._peerexecutor = self
661
625
662 self._futures.add(f)
626 self._futures.add(f)
663 self._calls.append((command, args, f))
627 self._calls.append((command, args, f))
664
628
665 return f
629 return f
666
630
667 def sendcommands(self):
631 def sendcommands(self):
668 if self._sent:
632 if self._sent:
669 return
633 return
670
634
671 if not self._calls:
635 if not self._calls:
672 return
636 return
673
637
674 self._sent = True
638 self._sent = True
675
639
676 # Unhack any future types so caller sees a clean type and so we
640 # Unhack any future types so caller sees a clean type and so we
677 # break reference cycle.
641 # break reference cycle.
678 for f in self._futures:
642 for f in self._futures:
679 if isinstance(f, queuedcommandfuture):
643 if isinstance(f, queuedcommandfuture):
680 f.__class__ = pycompat.futures.Future
644 f.__class__ = pycompat.futures.Future
681 f._peerexecutor = None
645 f._peerexecutor = None
682
646
683 # Mark the future as running and filter out cancelled futures.
647 # Mark the future as running and filter out cancelled futures.
684 calls = [(command, args, f)
648 calls = [(command, args, f)
685 for command, args, f in self._calls
649 for command, args, f in self._calls
686 if f.set_running_or_notify_cancel()]
650 if f.set_running_or_notify_cancel()]
687
651
688 # Clear out references, prevent improper object usage.
652 # Clear out references, prevent improper object usage.
689 self._calls = None
653 self._calls = None
690
654
691 if not calls:
655 if not calls:
692 return
656 return
693
657
694 permissions = set(self._neededpermissions)
658 permissions = set(self._neededpermissions)
695
659
696 if 'push' in permissions and 'pull' in permissions:
660 if 'push' in permissions and 'pull' in permissions:
697 permissions.remove('pull')
661 permissions.remove('pull')
698
662
699 if len(permissions) > 1:
663 if len(permissions) > 1:
700 raise error.RepoError(_('cannot make request requiring multiple '
664 raise error.RepoError(_('cannot make request requiring multiple '
701 'permissions: %s') %
665 'permissions: %s') %
702 _(', ').join(sorted(permissions)))
666 _(', ').join(sorted(permissions)))
703
667
704 permission = {
668 permission = {
705 'push': 'rw',
669 'push': 'rw',
706 'pull': 'ro',
670 'pull': 'ro',
707 }[permissions.pop()]
671 }[permissions.pop()]
708
672
709 handler, resp = sendv2request(
673 handler, resp = sendv2request(
710 self._ui, self._opener, self._requestbuilder, self._apiurl,
674 self._ui, self._opener, self._requestbuilder, self._apiurl,
711 permission, calls)
675 permission, calls)
712
676
713 # TODO we probably want to validate the HTTP code, media type, etc.
677 # TODO we probably want to validate the HTTP code, media type, etc.
714
678
715 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
679 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
716 self._responsef = self._responseexecutor.submit(self._handleresponse,
680 self._responsef = self._responseexecutor.submit(self._handleresponse,
717 handler, resp)
681 handler, resp)
718
682
719 def close(self):
683 def close(self):
720 if self._closed:
684 if self._closed:
721 return
685 return
722
686
723 self.sendcommands()
687 self.sendcommands()
724
688
725 self._closed = True
689 self._closed = True
726
690
727 if not self._responsef:
691 if not self._responsef:
728 return
692 return
729
693
730 # TODO ^C here may not result in immediate program termination.
694 # TODO ^C here may not result in immediate program termination.
731
695
732 try:
696 try:
733 self._responsef.result()
697 self._responsef.result()
734 finally:
698 finally:
735 self._responseexecutor.shutdown(wait=True)
699 self._responseexecutor.shutdown(wait=True)
736 self._responsef = None
700 self._responsef = None
737 self._responseexecutor = None
701 self._responseexecutor = None
738
702
739 # If any of our futures are still in progress, mark them as
703 # If any of our futures are still in progress, mark them as
740 # errored, otherwise a result() could wait indefinitely.
704 # errored, otherwise a result() could wait indefinitely.
741 for f in self._futures:
705 for f in self._futures:
742 if not f.done():
706 if not f.done():
743 f.set_exception(error.ResponseError(
707 f.set_exception(error.ResponseError(
744 _('unfulfilled command response')))
708 _('unfulfilled command response')))
745
709
746 self._futures = None
710 self._futures = None
747
711
748 def _handleresponse(self, handler, resp):
712 def _handleresponse(self, handler, resp):
749 # Called in a thread to read the response.
713 # Called in a thread to read the response.
750
714
751 while handler.readframe(resp):
715 while handler.readframe(resp):
752 pass
716 pass
753
717
754 # TODO implement interface for version 2 peers
718 # TODO implement interface for version 2 peers
755 @interfaceutil.implementer(repository.ipeerconnection,
719 @interfaceutil.implementer(repository.ipeerconnection,
756 repository.ipeercapabilities,
720 repository.ipeercapabilities,
757 repository.ipeerrequests)
721 repository.ipeerrequests)
758 class httpv2peer(object):
722 class httpv2peer(object):
759 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
723 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
760 apidescriptor):
724 apidescriptor):
761 self.ui = ui
725 self.ui = ui
762
726
763 if repourl.endswith('/'):
727 if repourl.endswith('/'):
764 repourl = repourl[:-1]
728 repourl = repourl[:-1]
765
729
766 self._url = repourl
730 self._url = repourl
767 self._apipath = apipath
731 self._apipath = apipath
768 self._apiurl = '%s/%s' % (repourl, apipath)
732 self._apiurl = '%s/%s' % (repourl, apipath)
769 self._opener = opener
733 self._opener = opener
770 self._requestbuilder = requestbuilder
734 self._requestbuilder = requestbuilder
771 self._descriptor = apidescriptor
735 self._descriptor = apidescriptor
772
736
773 # Start of ipeerconnection.
737 # Start of ipeerconnection.
774
738
775 def url(self):
739 def url(self):
776 return self._url
740 return self._url
777
741
778 def local(self):
742 def local(self):
779 return None
743 return None
780
744
781 def peer(self):
745 def peer(self):
782 return self
746 return self
783
747
784 def canpush(self):
748 def canpush(self):
785 # TODO change once implemented.
749 # TODO change once implemented.
786 return False
750 return False
787
751
788 def close(self):
752 def close(self):
789 pass
753 pass
790
754
791 # End of ipeerconnection.
755 # End of ipeerconnection.
792
756
793 # Start of ipeercapabilities.
757 # Start of ipeercapabilities.
794
758
795 def capable(self, name):
759 def capable(self, name):
796 # The capabilities used internally historically map to capabilities
760 # The capabilities used internally historically map to capabilities
797 # advertised from the "capabilities" wire protocol command. However,
761 # advertised from the "capabilities" wire protocol command. However,
798 # version 2 of that command works differently.
762 # version 2 of that command works differently.
799
763
800 # Maps to commands that are available.
764 # Maps to commands that are available.
801 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
765 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
802 return True
766 return True
803
767
804 # Other concepts.
768 # Other concepts.
805 if name in ('bundle2'):
769 if name in ('bundle2'):
806 return True
770 return True
807
771
808 # Alias command-* to presence of command of that name.
772 # Alias command-* to presence of command of that name.
809 if name.startswith('command-'):
773 if name.startswith('command-'):
810 return name[len('command-'):] in self._descriptor['commands']
774 return name[len('command-'):] in self._descriptor['commands']
811
775
812 return False
776 return False
813
777
814 def requirecap(self, name, purpose):
778 def requirecap(self, name, purpose):
815 if self.capable(name):
779 if self.capable(name):
816 return
780 return
817
781
818 raise error.CapabilityError(
782 raise error.CapabilityError(
819 _('cannot %s; client or remote repository does not support the %r '
783 _('cannot %s; client or remote repository does not support the %r '
820 'capability') % (purpose, name))
784 'capability') % (purpose, name))
821
785
822 # End of ipeercapabilities.
786 # End of ipeercapabilities.
823
787
824 def _call(self, name, **args):
788 def _call(self, name, **args):
825 with self.commandexecutor() as e:
789 with self.commandexecutor() as e:
826 return e.callcommand(name, args).result()
790 return e.callcommand(name, args).result()
827
791
828 def commandexecutor(self):
792 def commandexecutor(self):
829 return httpv2executor(self.ui, self._opener, self._requestbuilder,
793 return httpv2executor(self.ui, self._opener, self._requestbuilder,
830 self._apiurl, self._descriptor)
794 self._apiurl, self._descriptor)
831
795
832 # Registry of API service names to metadata about peers that handle it.
796 # Registry of API service names to metadata about peers that handle it.
833 #
797 #
834 # The following keys are meaningful:
798 # The following keys are meaningful:
835 #
799 #
836 # init
800 # init
837 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
801 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
838 # apidescriptor) to create a peer.
802 # apidescriptor) to create a peer.
839 #
803 #
840 # priority
804 # priority
841 # Integer priority for the service. If we could choose from multiple
805 # Integer priority for the service. If we could choose from multiple
842 # services, we choose the one with the highest priority.
806 # services, we choose the one with the highest priority.
843 API_PEERS = {
807 API_PEERS = {
844 wireprototypes.HTTP_WIREPROTO_V2: {
808 wireprototypes.HTTP_WIREPROTO_V2: {
845 'init': httpv2peer,
809 'init': httpv2peer,
846 'priority': 50,
810 'priority': 50,
847 },
811 },
848 }
812 }
849
813
850 def performhandshake(ui, url, opener, requestbuilder):
814 def performhandshake(ui, url, opener, requestbuilder):
851 # The handshake is a request to the capabilities command.
815 # The handshake is a request to the capabilities command.
852
816
853 caps = None
817 caps = None
854 def capable(x):
818 def capable(x):
855 raise error.ProgrammingError('should not be called')
819 raise error.ProgrammingError('should not be called')
856
820
857 args = {}
821 args = {}
858
822
859 # The client advertises support for newer protocols by adding an
823 # The client advertises support for newer protocols by adding an
860 # X-HgUpgrade-* header with a list of supported APIs and an
824 # X-HgUpgrade-* header with a list of supported APIs and an
861 # X-HgProto-* header advertising which serializing formats it supports.
825 # X-HgProto-* header advertising which serializing formats it supports.
862 # We only support the HTTP version 2 transport and CBOR responses for
826 # We only support the HTTP version 2 transport and CBOR responses for
863 # now.
827 # now.
864 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
828 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
865
829
866 if advertisev2:
830 if advertisev2:
867 args['headers'] = {
831 args['headers'] = {
868 r'X-HgProto-1': r'cbor',
832 r'X-HgProto-1': r'cbor',
869 }
833 }
870
834
871 args['headers'].update(
835 args['headers'].update(
872 encodevalueinheaders(' '.join(sorted(API_PEERS)),
836 encodevalueinheaders(' '.join(sorted(API_PEERS)),
873 'X-HgUpgrade',
837 'X-HgUpgrade',
874 # We don't know the header limit this early.
838 # We don't know the header limit this early.
875 # So make it small.
839 # So make it small.
876 1024))
840 1024))
877
841
878 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
842 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
879 capable, url, 'capabilities',
843 capable, url, 'capabilities',
880 args)
844 args)
881 resp = sendrequest(ui, opener, req)
845 resp = sendrequest(ui, opener, req)
882
846
883 # The server may redirect us to the repo root, stripping the
847 # The server may redirect us to the repo root, stripping the
884 # ?cmd=capabilities query string from the URL. The server would likely
848 # ?cmd=capabilities query string from the URL. The server would likely
885 # return HTML in this case and ``parsev1commandresponse()`` would raise.
849 # return HTML in this case and ``parsev1commandresponse()`` would raise.
886 # We catch this special case and re-issue the capabilities request against
850 # We catch this special case and re-issue the capabilities request against
887 # the new URL.
851 # the new URL.
888 #
852 #
889 # We should ideally not do this, as a redirect that drops the query
853 # We should ideally not do this, as a redirect that drops the query
890 # string from the URL is arguably a server bug. (Garbage in, garbage out).
854 # string from the URL is arguably a server bug. (Garbage in, garbage out).
891 # However, Mercurial clients for several years appeared to handle this
855 # However, Mercurial clients for several years appeared to handle this
892 # issue without behavior degradation. And according to issue 5860, it may
856 # issue without behavior degradation. And according to issue 5860, it may
893 # be a longstanding bug in some server implementations. So we allow a
857 # be a longstanding bug in some server implementations. So we allow a
894 # redirect that drops the query string to "just work."
858 # redirect that drops the query string to "just work."
895 try:
859 try:
896 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
860 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
897 compressible=False,
861 compressible=False,
898 allowcbor=advertisev2)
862 allowcbor=advertisev2)
899 except RedirectedRepoError as e:
863 except RedirectedRepoError as e:
900 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
864 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
901 capable, e.respurl,
865 capable, e.respurl,
902 'capabilities', args)
866 'capabilities', args)
903 resp = sendrequest(ui, opener, req)
867 resp = sendrequest(ui, opener, req)
904 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
868 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
905 compressible=False,
869 compressible=False,
906 allowcbor=advertisev2)
870 allowcbor=advertisev2)
907
871
908 try:
872 try:
909 rawdata = resp.read()
873 rawdata = resp.read()
910 finally:
874 finally:
911 resp.close()
875 resp.close()
912
876
913 if not ct.startswith('application/mercurial-'):
877 if not ct.startswith('application/mercurial-'):
914 raise error.ProgrammingError('unexpected content-type: %s' % ct)
878 raise error.ProgrammingError('unexpected content-type: %s' % ct)
915
879
916 if advertisev2:
880 if advertisev2:
917 if ct == 'application/mercurial-cbor':
881 if ct == 'application/mercurial-cbor':
918 try:
882 try:
919 info = cborutil.decodeall(rawdata)[0]
883 info = cborutil.decodeall(rawdata)[0]
920 except cborutil.CBORDecodeError:
884 except cborutil.CBORDecodeError:
921 raise error.Abort(_('error decoding CBOR from remote server'),
885 raise error.Abort(_('error decoding CBOR from remote server'),
922 hint=_('try again and consider contacting '
886 hint=_('try again and consider contacting '
923 'the server operator'))
887 'the server operator'))
924
888
925 # We got a legacy response. That's fine.
889 # We got a legacy response. That's fine.
926 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
890 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
927 info = {
891 info = {
928 'v1capabilities': set(rawdata.split())
892 'v1capabilities': set(rawdata.split())
929 }
893 }
930
894
931 else:
895 else:
932 raise error.RepoError(
896 raise error.RepoError(
933 _('unexpected response type from server: %s') % ct)
897 _('unexpected response type from server: %s') % ct)
934 else:
898 else:
935 info = {
899 info = {
936 'v1capabilities': set(rawdata.split())
900 'v1capabilities': set(rawdata.split())
937 }
901 }
938
902
939 return respurl, info
903 return respurl, info
940
904
941 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
905 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
942 """Construct an appropriate HTTP peer instance.
906 """Construct an appropriate HTTP peer instance.
943
907
944 ``opener`` is an ``url.opener`` that should be used to establish
908 ``opener`` is an ``url.opener`` that should be used to establish
945 connections, perform HTTP requests.
909 connections, perform HTTP requests.
946
910
947 ``requestbuilder`` is the type used for constructing HTTP requests.
911 ``requestbuilder`` is the type used for constructing HTTP requests.
948 It exists as an argument so extensions can override the default.
912 It exists as an argument so extensions can override the default.
949 """
913 """
950 u = util.url(path)
914 u = util.url(path)
951 if u.query or u.fragment:
915 if u.query or u.fragment:
952 raise error.Abort(_('unsupported URL component: "%s"') %
916 raise error.Abort(_('unsupported URL component: "%s"') %
953 (u.query or u.fragment))
917 (u.query or u.fragment))
954
918
955 # urllib cannot handle URLs with embedded user or passwd.
919 # urllib cannot handle URLs with embedded user or passwd.
956 url, authinfo = u.authinfo()
920 url, authinfo = u.authinfo()
957 ui.debug('using %s\n' % url)
921 ui.debug('using %s\n' % url)
958
922
959 opener = opener or urlmod.opener(ui, authinfo)
923 opener = opener or urlmod.opener(ui, authinfo)
960
924
961 respurl, info = performhandshake(ui, url, opener, requestbuilder)
925 respurl, info = performhandshake(ui, url, opener, requestbuilder)
962
926
963 # Given the intersection of APIs that both we and the server support,
927 # Given the intersection of APIs that both we and the server support,
964 # sort by their advertised priority and pick the first one.
928 # sort by their advertised priority and pick the first one.
965 #
929 #
966 # TODO consider making this request-based and interface driven. For
930 # TODO consider making this request-based and interface driven. For
967 # example, the caller could say "I want a peer that does X." It's quite
931 # example, the caller could say "I want a peer that does X." It's quite
968 # possible that not all peers would do that. Since we know the service
932 # possible that not all peers would do that. Since we know the service
969 # capabilities, we could filter out services not meeting the
933 # capabilities, we could filter out services not meeting the
970 # requirements. Possibly by consulting the interfaces defined by the
934 # requirements. Possibly by consulting the interfaces defined by the
971 # peer type.
935 # peer type.
972 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
936 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
973
937
974 preferredchoices = sorted(apipeerchoices,
938 preferredchoices = sorted(apipeerchoices,
975 key=lambda x: API_PEERS[x]['priority'],
939 key=lambda x: API_PEERS[x]['priority'],
976 reverse=True)
940 reverse=True)
977
941
978 for service in preferredchoices:
942 for service in preferredchoices:
979 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
943 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
980
944
981 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
945 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
982 requestbuilder,
946 requestbuilder,
983 info['apis'][service])
947 info['apis'][service])
984
948
985 # Failed to construct an API peer. Fall back to legacy.
949 # Failed to construct an API peer. Fall back to legacy.
986 return httppeer(ui, path, respurl, opener, requestbuilder,
950 return httppeer(ui, path, respurl, opener, requestbuilder,
987 info['v1capabilities'])
951 info['v1capabilities'])
988
952
989 def instance(ui, path, create, intents=None, createopts=None):
953 def instance(ui, path, create, intents=None, createopts=None):
990 if create:
954 if create:
991 raise error.Abort(_('cannot create new http repository'))
955 raise error.Abort(_('cannot create new http repository'))
992 try:
956 try:
993 if path.startswith('https:') and not urlmod.has_https:
957 if path.startswith('https:') and not urlmod.has_https:
994 raise error.Abort(_('Python support for SSL and HTTPS '
958 raise error.Abort(_('Python support for SSL and HTTPS '
995 'is not installed'))
959 'is not installed'))
996
960
997 inst = makepeer(ui, path)
961 inst = makepeer(ui, path)
998
962
999 return inst
963 return inst
1000 except error.RepoError as httpexception:
964 except error.RepoError as httpexception:
1001 try:
965 try:
1002 r = statichttprepo.instance(ui, "static-" + path, create)
966 r = statichttprepo.instance(ui, "static-" + path, create)
1003 ui.note(_('(falling back to static-http)\n'))
967 ui.note(_('(falling back to static-http)\n'))
1004 return r
968 return r
1005 except error.RepoError:
969 except error.RepoError:
1006 raise httpexception # use the original http RepoError instead
970 raise httpexception # use the original http RepoError instead
@@ -1,597 +1,633 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 os
13 import os
14 import socket
14 import socket
15 import sys
15 import sys
16
16
17 from .i18n import _
17 from .i18n import _
18 from . import (
18 from . import (
19 encoding,
19 encoding,
20 error,
20 error,
21 httpconnection as httpconnectionmod,
21 httpconnection as httpconnectionmod,
22 keepalive,
22 keepalive,
23 pycompat,
23 pycompat,
24 sslutil,
24 sslutil,
25 urllibcompat,
25 urllibcompat,
26 util,
26 util,
27 )
27 )
28 from .utils import (
28 from .utils import (
29 stringutil,
29 stringutil,
30 )
30 )
31
31
32 httplib = util.httplib
32 httplib = util.httplib
33 stringio = util.stringio
33 stringio = util.stringio
34 urlerr = util.urlerr
34 urlerr = util.urlerr
35 urlreq = util.urlreq
35 urlreq = util.urlreq
36
36
37 def escape(s, quote=None):
37 def escape(s, quote=None):
38 '''Replace special characters "&", "<" and ">" to HTML-safe sequences.
38 '''Replace special characters "&", "<" and ">" to HTML-safe sequences.
39 If the optional flag quote is true, the quotation mark character (")
39 If the optional flag quote is true, the quotation mark character (")
40 is also translated.
40 is also translated.
41
41
42 This is the same as cgi.escape in Python, but always operates on
42 This is the same as cgi.escape in Python, but always operates on
43 bytes, whereas cgi.escape in Python 3 only works on unicodes.
43 bytes, whereas cgi.escape in Python 3 only works on unicodes.
44 '''
44 '''
45 s = s.replace(b"&", b"&amp;")
45 s = s.replace(b"&", b"&amp;")
46 s = s.replace(b"<", b"&lt;")
46 s = s.replace(b"<", b"&lt;")
47 s = s.replace(b">", b"&gt;")
47 s = s.replace(b">", b"&gt;")
48 if quote:
48 if quote:
49 s = s.replace(b'"', b"&quot;")
49 s = s.replace(b'"', b"&quot;")
50 return s
50 return s
51
51
52 class passwordmgr(object):
52 class passwordmgr(object):
53 def __init__(self, ui, passwddb):
53 def __init__(self, ui, passwddb):
54 self.ui = ui
54 self.ui = ui
55 self.passwddb = passwddb
55 self.passwddb = passwddb
56
56
57 def add_password(self, realm, uri, user, passwd):
57 def add_password(self, realm, uri, user, passwd):
58 return self.passwddb.add_password(realm, uri, user, passwd)
58 return self.passwddb.add_password(realm, uri, user, passwd)
59
59
60 def find_user_password(self, realm, authuri):
60 def find_user_password(self, realm, authuri):
61 authinfo = self.passwddb.find_user_password(realm, authuri)
61 authinfo = self.passwddb.find_user_password(realm, authuri)
62 user, passwd = authinfo
62 user, passwd = authinfo
63 if user and passwd:
63 if user and passwd:
64 self._writedebug(user, passwd)
64 self._writedebug(user, passwd)
65 return (user, passwd)
65 return (user, passwd)
66
66
67 if not user or not passwd:
67 if not user or not passwd:
68 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
68 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
69 if res:
69 if res:
70 group, auth = res
70 group, auth = res
71 user, passwd = auth.get('username'), auth.get('password')
71 user, passwd = auth.get('username'), auth.get('password')
72 self.ui.debug("using auth.%s.* for authentication\n" % group)
72 self.ui.debug("using auth.%s.* for authentication\n" % group)
73 if not user or not passwd:
73 if not user or not passwd:
74 u = util.url(pycompat.bytesurl(authuri))
74 u = util.url(pycompat.bytesurl(authuri))
75 u.query = None
75 u.query = None
76 if not self.ui.interactive():
76 if not self.ui.interactive():
77 raise error.Abort(_('http authorization required for %s') %
77 raise error.Abort(_('http authorization required for %s') %
78 util.hidepassword(bytes(u)))
78 util.hidepassword(bytes(u)))
79
79
80 self.ui.write(_("http authorization required for %s\n") %
80 self.ui.write(_("http authorization required for %s\n") %
81 util.hidepassword(bytes(u)))
81 util.hidepassword(bytes(u)))
82 self.ui.write(_("realm: %s\n") % pycompat.bytesurl(realm))
82 self.ui.write(_("realm: %s\n") % pycompat.bytesurl(realm))
83 if user:
83 if user:
84 self.ui.write(_("user: %s\n") % user)
84 self.ui.write(_("user: %s\n") % user)
85 else:
85 else:
86 user = self.ui.prompt(_("user:"), default=None)
86 user = self.ui.prompt(_("user:"), default=None)
87
87
88 if not passwd:
88 if not passwd:
89 passwd = self.ui.getpass()
89 passwd = self.ui.getpass()
90
90
91 self.passwddb.add_password(realm, authuri, user, passwd)
91 self.passwddb.add_password(realm, authuri, user, passwd)
92 self._writedebug(user, passwd)
92 self._writedebug(user, passwd)
93 return (user, passwd)
93 return (user, passwd)
94
94
95 def _writedebug(self, user, passwd):
95 def _writedebug(self, user, passwd):
96 msg = _('http auth: user %s, password %s\n')
96 msg = _('http auth: user %s, password %s\n')
97 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
97 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
98
98
99 def find_stored_password(self, authuri):
99 def find_stored_password(self, authuri):
100 return self.passwddb.find_user_password(None, authuri)
100 return self.passwddb.find_user_password(None, authuri)
101
101
102 class proxyhandler(urlreq.proxyhandler):
102 class proxyhandler(urlreq.proxyhandler):
103 def __init__(self, ui):
103 def __init__(self, ui):
104 proxyurl = (ui.config("http_proxy", "host") or
104 proxyurl = (ui.config("http_proxy", "host") or
105 encoding.environ.get('http_proxy'))
105 encoding.environ.get('http_proxy'))
106 # XXX proxyauthinfo = None
106 # XXX proxyauthinfo = None
107
107
108 if proxyurl:
108 if proxyurl:
109 # proxy can be proper url or host[:port]
109 # proxy can be proper url or host[:port]
110 if not (proxyurl.startswith('http:') or
110 if not (proxyurl.startswith('http:') or
111 proxyurl.startswith('https:')):
111 proxyurl.startswith('https:')):
112 proxyurl = 'http://' + proxyurl + '/'
112 proxyurl = 'http://' + proxyurl + '/'
113 proxy = util.url(proxyurl)
113 proxy = util.url(proxyurl)
114 if not proxy.user:
114 if not proxy.user:
115 proxy.user = ui.config("http_proxy", "user")
115 proxy.user = ui.config("http_proxy", "user")
116 proxy.passwd = ui.config("http_proxy", "passwd")
116 proxy.passwd = ui.config("http_proxy", "passwd")
117
117
118 # see if we should use a proxy for this url
118 # see if we should use a proxy for this url
119 no_list = ["localhost", "127.0.0.1"]
119 no_list = ["localhost", "127.0.0.1"]
120 no_list.extend([p.lower() for
120 no_list.extend([p.lower() for
121 p in ui.configlist("http_proxy", "no")])
121 p in ui.configlist("http_proxy", "no")])
122 no_list.extend([p.strip().lower() for
122 no_list.extend([p.strip().lower() for
123 p in encoding.environ.get("no_proxy", '').split(',')
123 p in encoding.environ.get("no_proxy", '').split(',')
124 if p.strip()])
124 if p.strip()])
125 # "http_proxy.always" config is for running tests on localhost
125 # "http_proxy.always" config is for running tests on localhost
126 if ui.configbool("http_proxy", "always"):
126 if ui.configbool("http_proxy", "always"):
127 self.no_list = []
127 self.no_list = []
128 else:
128 else:
129 self.no_list = no_list
129 self.no_list = no_list
130
130
131 proxyurl = bytes(proxy)
131 proxyurl = bytes(proxy)
132 proxies = {'http': proxyurl, 'https': proxyurl}
132 proxies = {'http': proxyurl, 'https': proxyurl}
133 ui.debug('proxying through %s\n' % util.hidepassword(proxyurl))
133 ui.debug('proxying through %s\n' % util.hidepassword(proxyurl))
134 else:
134 else:
135 proxies = {}
135 proxies = {}
136
136
137 urlreq.proxyhandler.__init__(self, proxies)
137 urlreq.proxyhandler.__init__(self, proxies)
138 self.ui = ui
138 self.ui = ui
139
139
140 def proxy_open(self, req, proxy, type_):
140 def proxy_open(self, req, proxy, type_):
141 host = urllibcompat.gethost(req).split(':')[0]
141 host = urllibcompat.gethost(req).split(':')[0]
142 for e in self.no_list:
142 for e in self.no_list:
143 if host == e:
143 if host == e:
144 return None
144 return None
145 if e.startswith('*.') and host.endswith(e[2:]):
145 if e.startswith('*.') and host.endswith(e[2:]):
146 return None
146 return None
147 if e.startswith('.') and host.endswith(e[1:]):
147 if e.startswith('.') and host.endswith(e[1:]):
148 return None
148 return None
149
149
150 return urlreq.proxyhandler.proxy_open(self, req, proxy, type_)
150 return urlreq.proxyhandler.proxy_open(self, req, proxy, type_)
151
151
152 def _gen_sendfile(orgsend):
152 def _gen_sendfile(orgsend):
153 def _sendfile(self, data):
153 def _sendfile(self, data):
154 # send a file
154 # send a file
155 if isinstance(data, httpconnectionmod.httpsendfile):
155 if isinstance(data, httpconnectionmod.httpsendfile):
156 # if auth required, some data sent twice, so rewind here
156 # if auth required, some data sent twice, so rewind here
157 data.seek(0)
157 data.seek(0)
158 for chunk in util.filechunkiter(data):
158 for chunk in util.filechunkiter(data):
159 orgsend(self, chunk)
159 orgsend(self, chunk)
160 else:
160 else:
161 orgsend(self, data)
161 orgsend(self, data)
162 return _sendfile
162 return _sendfile
163
163
164 has_https = util.safehasattr(urlreq, 'httpshandler')
164 has_https = util.safehasattr(urlreq, 'httpshandler')
165
165
166 class httpconnection(keepalive.HTTPConnection):
166 class httpconnection(keepalive.HTTPConnection):
167 # must be able to send big bundle as stream.
167 # must be able to send big bundle as stream.
168 send = _gen_sendfile(keepalive.HTTPConnection.send)
168 send = _gen_sendfile(keepalive.HTTPConnection.send)
169
169
170 def getresponse(self):
170 def getresponse(self):
171 proxyres = getattr(self, 'proxyres', None)
171 proxyres = getattr(self, 'proxyres', None)
172 if proxyres:
172 if proxyres:
173 if proxyres.will_close:
173 if proxyres.will_close:
174 self.close()
174 self.close()
175 self.proxyres = None
175 self.proxyres = None
176 return proxyres
176 return proxyres
177 return keepalive.HTTPConnection.getresponse(self)
177 return keepalive.HTTPConnection.getresponse(self)
178
178
179 # general transaction handler to support different ways to handle
179 # general transaction handler to support different ways to handle
180 # HTTPS proxying before and after Python 2.6.3.
180 # HTTPS proxying before and after Python 2.6.3.
181 def _generic_start_transaction(handler, h, req):
181 def _generic_start_transaction(handler, h, req):
182 tunnel_host = getattr(req, '_tunnel_host', None)
182 tunnel_host = getattr(req, '_tunnel_host', None)
183 if tunnel_host:
183 if tunnel_host:
184 if tunnel_host[:7] not in ['http://', 'https:/']:
184 if tunnel_host[:7] not in ['http://', 'https:/']:
185 tunnel_host = 'https://' + tunnel_host
185 tunnel_host = 'https://' + tunnel_host
186 new_tunnel = True
186 new_tunnel = True
187 else:
187 else:
188 tunnel_host = urllibcompat.getselector(req)
188 tunnel_host = urllibcompat.getselector(req)
189 new_tunnel = False
189 new_tunnel = False
190
190
191 if new_tunnel or tunnel_host == urllibcompat.getfullurl(req): # has proxy
191 if new_tunnel or tunnel_host == urllibcompat.getfullurl(req): # has proxy
192 u = util.url(tunnel_host)
192 u = util.url(tunnel_host)
193 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
193 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
194 h.realhostport = ':'.join([u.host, (u.port or '443')])
194 h.realhostport = ':'.join([u.host, (u.port or '443')])
195 h.headers = req.headers.copy()
195 h.headers = req.headers.copy()
196 h.headers.update(handler.parent.addheaders)
196 h.headers.update(handler.parent.addheaders)
197 return
197 return
198
198
199 h.realhostport = None
199 h.realhostport = None
200 h.headers = None
200 h.headers = None
201
201
202 def _generic_proxytunnel(self):
202 def _generic_proxytunnel(self):
203 proxyheaders = dict(
203 proxyheaders = dict(
204 [(x, self.headers[x]) for x in self.headers
204 [(x, self.headers[x]) for x in self.headers
205 if x.lower().startswith('proxy-')])
205 if x.lower().startswith('proxy-')])
206 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
206 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
207 for header in proxyheaders.iteritems():
207 for header in proxyheaders.iteritems():
208 self.send('%s: %s\r\n' % header)
208 self.send('%s: %s\r\n' % header)
209 self.send('\r\n')
209 self.send('\r\n')
210
210
211 # majority of the following code is duplicated from
211 # majority of the following code is duplicated from
212 # httplib.HTTPConnection as there are no adequate places to
212 # httplib.HTTPConnection as there are no adequate places to
213 # override functions to provide the needed functionality
213 # override functions to provide the needed functionality
214 res = self.response_class(self.sock,
214 res = self.response_class(self.sock,
215 strict=self.strict,
215 strict=self.strict,
216 method=self._method)
216 method=self._method)
217
217
218 while True:
218 while True:
219 version, status, reason = res._read_status()
219 version, status, reason = res._read_status()
220 if status != httplib.CONTINUE:
220 if status != httplib.CONTINUE:
221 break
221 break
222 # skip lines that are all whitespace
222 # skip lines that are all whitespace
223 list(iter(lambda: res.fp.readline().strip(), ''))
223 list(iter(lambda: res.fp.readline().strip(), ''))
224 res.status = status
224 res.status = status
225 res.reason = reason.strip()
225 res.reason = reason.strip()
226
226
227 if res.status == 200:
227 if res.status == 200:
228 # skip lines until we find a blank line
228 # skip lines until we find a blank line
229 list(iter(res.fp.readline, '\r\n'))
229 list(iter(res.fp.readline, '\r\n'))
230 return True
230 return True
231
231
232 if version == 'HTTP/1.0':
232 if version == 'HTTP/1.0':
233 res.version = 10
233 res.version = 10
234 elif version.startswith('HTTP/1.'):
234 elif version.startswith('HTTP/1.'):
235 res.version = 11
235 res.version = 11
236 elif version == 'HTTP/0.9':
236 elif version == 'HTTP/0.9':
237 res.version = 9
237 res.version = 9
238 else:
238 else:
239 raise httplib.UnknownProtocol(version)
239 raise httplib.UnknownProtocol(version)
240
240
241 if res.version == 9:
241 if res.version == 9:
242 res.length = None
242 res.length = None
243 res.chunked = 0
243 res.chunked = 0
244 res.will_close = 1
244 res.will_close = 1
245 res.msg = httplib.HTTPMessage(stringio())
245 res.msg = httplib.HTTPMessage(stringio())
246 return False
246 return False
247
247
248 res.msg = httplib.HTTPMessage(res.fp)
248 res.msg = httplib.HTTPMessage(res.fp)
249 res.msg.fp = None
249 res.msg.fp = None
250
250
251 # are we using the chunked-style of transfer encoding?
251 # are we using the chunked-style of transfer encoding?
252 trenc = res.msg.getheader('transfer-encoding')
252 trenc = res.msg.getheader('transfer-encoding')
253 if trenc and trenc.lower() == "chunked":
253 if trenc and trenc.lower() == "chunked":
254 res.chunked = 1
254 res.chunked = 1
255 res.chunk_left = None
255 res.chunk_left = None
256 else:
256 else:
257 res.chunked = 0
257 res.chunked = 0
258
258
259 # will the connection close at the end of the response?
259 # will the connection close at the end of the response?
260 res.will_close = res._check_close()
260 res.will_close = res._check_close()
261
261
262 # do we have a Content-Length?
262 # do we have a Content-Length?
263 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
263 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
264 # transfer-encoding is "chunked"
264 # transfer-encoding is "chunked"
265 length = res.msg.getheader('content-length')
265 length = res.msg.getheader('content-length')
266 if length and not res.chunked:
266 if length and not res.chunked:
267 try:
267 try:
268 res.length = int(length)
268 res.length = int(length)
269 except ValueError:
269 except ValueError:
270 res.length = None
270 res.length = None
271 else:
271 else:
272 if res.length < 0: # ignore nonsensical negative lengths
272 if res.length < 0: # ignore nonsensical negative lengths
273 res.length = None
273 res.length = None
274 else:
274 else:
275 res.length = None
275 res.length = None
276
276
277 # does the body have a fixed length? (of zero)
277 # does the body have a fixed length? (of zero)
278 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
278 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
279 100 <= status < 200 or # 1xx codes
279 100 <= status < 200 or # 1xx codes
280 res._method == 'HEAD'):
280 res._method == 'HEAD'):
281 res.length = 0
281 res.length = 0
282
282
283 # if the connection remains open, and we aren't using chunked, and
283 # if the connection remains open, and we aren't using chunked, and
284 # a content-length was not provided, then assume that the connection
284 # a content-length was not provided, then assume that the connection
285 # WILL close.
285 # WILL close.
286 if (not res.will_close and
286 if (not res.will_close and
287 not res.chunked and
287 not res.chunked and
288 res.length is None):
288 res.length is None):
289 res.will_close = 1
289 res.will_close = 1
290
290
291 self.proxyres = res
291 self.proxyres = res
292
292
293 return False
293 return False
294
294
295 class httphandler(keepalive.HTTPHandler):
295 class httphandler(keepalive.HTTPHandler):
296 def http_open(self, req):
296 def http_open(self, req):
297 return self.do_open(httpconnection, req)
297 return self.do_open(httpconnection, req)
298
298
299 def _start_transaction(self, h, req):
299 def _start_transaction(self, h, req):
300 _generic_start_transaction(self, h, req)
300 _generic_start_transaction(self, h, req)
301 return keepalive.HTTPHandler._start_transaction(self, h, req)
301 return keepalive.HTTPHandler._start_transaction(self, h, req)
302
302
303 class logginghttpconnection(keepalive.HTTPConnection):
303 class logginghttpconnection(keepalive.HTTPConnection):
304 def __init__(self, createconn, *args, **kwargs):
304 def __init__(self, createconn, *args, **kwargs):
305 keepalive.HTTPConnection.__init__(self, *args, **kwargs)
305 keepalive.HTTPConnection.__init__(self, *args, **kwargs)
306 self._create_connection = createconn
306 self._create_connection = createconn
307
307
308 if sys.version_info < (2, 7, 7):
308 if sys.version_info < (2, 7, 7):
309 # copied from 2.7.14, since old implementations directly call
309 # copied from 2.7.14, since old implementations directly call
310 # socket.create_connection()
310 # socket.create_connection()
311 def connect(self):
311 def connect(self):
312 self.sock = self._create_connection((self.host, self.port),
312 self.sock = self._create_connection((self.host, self.port),
313 self.timeout,
313 self.timeout,
314 self.source_address)
314 self.source_address)
315 if self._tunnel_host:
315 if self._tunnel_host:
316 self._tunnel()
316 self._tunnel()
317
317
318 class logginghttphandler(httphandler):
318 class logginghttphandler(httphandler):
319 """HTTP handler that logs socket I/O."""
319 """HTTP handler that logs socket I/O."""
320 def __init__(self, logfh, name, observeropts):
320 def __init__(self, logfh, name, observeropts):
321 super(logginghttphandler, self).__init__()
321 super(logginghttphandler, self).__init__()
322
322
323 self._logfh = logfh
323 self._logfh = logfh
324 self._logname = name
324 self._logname = name
325 self._observeropts = observeropts
325 self._observeropts = observeropts
326
326
327 # do_open() calls the passed class to instantiate an HTTPConnection. We
327 # do_open() calls the passed class to instantiate an HTTPConnection. We
328 # pass in a callable method that creates a custom HTTPConnection instance
328 # pass in a callable method that creates a custom HTTPConnection instance
329 # whose callback to create the socket knows how to proxy the socket.
329 # whose callback to create the socket knows how to proxy the socket.
330 def http_open(self, req):
330 def http_open(self, req):
331 return self.do_open(self._makeconnection, req)
331 return self.do_open(self._makeconnection, req)
332
332
333 def _makeconnection(self, *args, **kwargs):
333 def _makeconnection(self, *args, **kwargs):
334 def createconnection(*args, **kwargs):
334 def createconnection(*args, **kwargs):
335 sock = socket.create_connection(*args, **kwargs)
335 sock = socket.create_connection(*args, **kwargs)
336 return util.makeloggingsocket(self._logfh, sock, self._logname,
336 return util.makeloggingsocket(self._logfh, sock, self._logname,
337 **self._observeropts)
337 **self._observeropts)
338
338
339 return logginghttpconnection(createconnection, *args, **kwargs)
339 return logginghttpconnection(createconnection, *args, **kwargs)
340
340
341 if has_https:
341 if has_https:
342 class httpsconnection(httplib.HTTPConnection):
342 class httpsconnection(httplib.HTTPConnection):
343 response_class = keepalive.HTTPResponse
343 response_class = keepalive.HTTPResponse
344 default_port = httplib.HTTPS_PORT
344 default_port = httplib.HTTPS_PORT
345 # must be able to send big bundle as stream.
345 # must be able to send big bundle as stream.
346 send = _gen_sendfile(keepalive.safesend)
346 send = _gen_sendfile(keepalive.safesend)
347 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
347 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
348
348
349 def __init__(self, host, port=None, key_file=None, cert_file=None,
349 def __init__(self, host, port=None, key_file=None, cert_file=None,
350 *args, **kwargs):
350 *args, **kwargs):
351 httplib.HTTPConnection.__init__(self, host, port, *args, **kwargs)
351 httplib.HTTPConnection.__init__(self, host, port, *args, **kwargs)
352 self.key_file = key_file
352 self.key_file = key_file
353 self.cert_file = cert_file
353 self.cert_file = cert_file
354
354
355 def connect(self):
355 def connect(self):
356 self.sock = socket.create_connection((self.host, self.port))
356 self.sock = socket.create_connection((self.host, self.port))
357
357
358 host = self.host
358 host = self.host
359 if self.realhostport: # use CONNECT proxy
359 if self.realhostport: # use CONNECT proxy
360 _generic_proxytunnel(self)
360 _generic_proxytunnel(self)
361 host = self.realhostport.rsplit(':', 1)[0]
361 host = self.realhostport.rsplit(':', 1)[0]
362 self.sock = sslutil.wrapsocket(
362 self.sock = sslutil.wrapsocket(
363 self.sock, self.key_file, self.cert_file, ui=self.ui,
363 self.sock, self.key_file, self.cert_file, ui=self.ui,
364 serverhostname=host)
364 serverhostname=host)
365 sslutil.validatesocket(self.sock)
365 sslutil.validatesocket(self.sock)
366
366
367 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
367 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
368 def __init__(self, ui):
368 def __init__(self, ui):
369 keepalive.KeepAliveHandler.__init__(self)
369 keepalive.KeepAliveHandler.__init__(self)
370 urlreq.httpshandler.__init__(self)
370 urlreq.httpshandler.__init__(self)
371 self.ui = ui
371 self.ui = ui
372 self.pwmgr = passwordmgr(self.ui,
372 self.pwmgr = passwordmgr(self.ui,
373 self.ui.httppasswordmgrdb)
373 self.ui.httppasswordmgrdb)
374
374
375 def _start_transaction(self, h, req):
375 def _start_transaction(self, h, req):
376 _generic_start_transaction(self, h, req)
376 _generic_start_transaction(self, h, req)
377 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
377 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
378
378
379 def https_open(self, req):
379 def https_open(self, req):
380 # urllibcompat.getfullurl() does not contain credentials
380 # urllibcompat.getfullurl() does not contain credentials
381 # and we may need them to match the certificates.
381 # and we may need them to match the certificates.
382 url = urllibcompat.getfullurl(req)
382 url = urllibcompat.getfullurl(req)
383 user, password = self.pwmgr.find_stored_password(url)
383 user, password = self.pwmgr.find_stored_password(url)
384 res = httpconnectionmod.readauthforuri(self.ui, url, user)
384 res = httpconnectionmod.readauthforuri(self.ui, url, user)
385 if res:
385 if res:
386 group, auth = res
386 group, auth = res
387 self.auth = auth
387 self.auth = auth
388 self.ui.debug("using auth.%s.* for authentication\n" % group)
388 self.ui.debug("using auth.%s.* for authentication\n" % group)
389 else:
389 else:
390 self.auth = None
390 self.auth = None
391 return self.do_open(self._makeconnection, req)
391 return self.do_open(self._makeconnection, req)
392
392
393 def _makeconnection(self, host, port=None, *args, **kwargs):
393 def _makeconnection(self, host, port=None, *args, **kwargs):
394 keyfile = None
394 keyfile = None
395 certfile = None
395 certfile = None
396
396
397 if len(args) >= 1: # key_file
397 if len(args) >= 1: # key_file
398 keyfile = args[0]
398 keyfile = args[0]
399 if len(args) >= 2: # cert_file
399 if len(args) >= 2: # cert_file
400 certfile = args[1]
400 certfile = args[1]
401 args = args[2:]
401 args = args[2:]
402
402
403 # if the user has specified different key/cert files in
403 # if the user has specified different key/cert files in
404 # hgrc, we prefer these
404 # hgrc, we prefer these
405 if self.auth and 'key' in self.auth and 'cert' in self.auth:
405 if self.auth and 'key' in self.auth and 'cert' in self.auth:
406 keyfile = self.auth['key']
406 keyfile = self.auth['key']
407 certfile = self.auth['cert']
407 certfile = self.auth['cert']
408
408
409 conn = httpsconnection(host, port, keyfile, certfile, *args,
409 conn = httpsconnection(host, port, keyfile, certfile, *args,
410 **kwargs)
410 **kwargs)
411 conn.ui = self.ui
411 conn.ui = self.ui
412 return conn
412 return conn
413
413
414 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
414 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
415 def __init__(self, *args, **kwargs):
415 def __init__(self, *args, **kwargs):
416 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
416 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
417 self.retried_req = None
417 self.retried_req = None
418
418
419 def reset_retry_count(self):
419 def reset_retry_count(self):
420 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
420 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
421 # forever. We disable reset_retry_count completely and reset in
421 # forever. We disable reset_retry_count completely and reset in
422 # http_error_auth_reqed instead.
422 # http_error_auth_reqed instead.
423 pass
423 pass
424
424
425 def http_error_auth_reqed(self, auth_header, host, req, headers):
425 def http_error_auth_reqed(self, auth_header, host, req, headers):
426 # Reset the retry counter once for each request.
426 # Reset the retry counter once for each request.
427 if req is not self.retried_req:
427 if req is not self.retried_req:
428 self.retried_req = req
428 self.retried_req = req
429 self.retried = 0
429 self.retried = 0
430 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
430 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
431 self, auth_header, host, req, headers)
431 self, auth_header, host, req, headers)
432
432
433 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
433 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
434 def __init__(self, *args, **kwargs):
434 def __init__(self, *args, **kwargs):
435 self.auth = None
435 self.auth = None
436 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
436 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
437 self.retried_req = None
437 self.retried_req = None
438
438
439 def http_request(self, request):
439 def http_request(self, request):
440 if self.auth:
440 if self.auth:
441 request.add_unredirected_header(self.auth_header, self.auth)
441 request.add_unredirected_header(self.auth_header, self.auth)
442
442
443 return request
443 return request
444
444
445 def https_request(self, request):
445 def https_request(self, request):
446 if self.auth:
446 if self.auth:
447 request.add_unredirected_header(self.auth_header, self.auth)
447 request.add_unredirected_header(self.auth_header, self.auth)
448
448
449 return request
449 return request
450
450
451 def reset_retry_count(self):
451 def reset_retry_count(self):
452 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
452 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
453 # forever. We disable reset_retry_count completely and reset in
453 # forever. We disable reset_retry_count completely and reset in
454 # http_error_auth_reqed instead.
454 # http_error_auth_reqed instead.
455 pass
455 pass
456
456
457 def http_error_auth_reqed(self, auth_header, host, req, headers):
457 def http_error_auth_reqed(self, auth_header, host, req, headers):
458 # Reset the retry counter once for each request.
458 # Reset the retry counter once for each request.
459 if req is not self.retried_req:
459 if req is not self.retried_req:
460 self.retried_req = req
460 self.retried_req = req
461 self.retried = 0
461 self.retried = 0
462 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
462 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
463 self, auth_header, host, req, headers)
463 self, auth_header, host, req, headers)
464
464
465 def retry_http_basic_auth(self, host, req, realm):
465 def retry_http_basic_auth(self, host, req, realm):
466 user, pw = self.passwd.find_user_password(
466 user, pw = self.passwd.find_user_password(
467 realm, urllibcompat.getfullurl(req))
467 realm, urllibcompat.getfullurl(req))
468 if pw is not None:
468 if pw is not None:
469 raw = "%s:%s" % (pycompat.bytesurl(user), pycompat.bytesurl(pw))
469 raw = "%s:%s" % (pycompat.bytesurl(user), pycompat.bytesurl(pw))
470 auth = r'Basic %s' % pycompat.strurl(base64.b64encode(raw).strip())
470 auth = r'Basic %s' % pycompat.strurl(base64.b64encode(raw).strip())
471 if req.get_header(self.auth_header, None) == auth:
471 if req.get_header(self.auth_header, None) == auth:
472 return None
472 return None
473 self.auth = auth
473 self.auth = auth
474 req.add_unredirected_header(self.auth_header, auth)
474 req.add_unredirected_header(self.auth_header, auth)
475 return self.parent.open(req)
475 return self.parent.open(req)
476 else:
476 else:
477 return None
477 return None
478
478
479 class cookiehandler(urlreq.basehandler):
479 class cookiehandler(urlreq.basehandler):
480 def __init__(self, ui):
480 def __init__(self, ui):
481 self.cookiejar = None
481 self.cookiejar = None
482
482
483 cookiefile = ui.config('auth', 'cookiefile')
483 cookiefile = ui.config('auth', 'cookiefile')
484 if not cookiefile:
484 if not cookiefile:
485 return
485 return
486
486
487 cookiefile = util.expandpath(cookiefile)
487 cookiefile = util.expandpath(cookiefile)
488 try:
488 try:
489 cookiejar = util.cookielib.MozillaCookieJar(
489 cookiejar = util.cookielib.MozillaCookieJar(
490 pycompat.fsdecode(cookiefile))
490 pycompat.fsdecode(cookiefile))
491 cookiejar.load()
491 cookiejar.load()
492 self.cookiejar = cookiejar
492 self.cookiejar = cookiejar
493 except util.cookielib.LoadError as e:
493 except util.cookielib.LoadError as e:
494 ui.warn(_('(error loading cookie file %s: %s; continuing without '
494 ui.warn(_('(error loading cookie file %s: %s; continuing without '
495 'cookies)\n') % (cookiefile, stringutil.forcebytestr(e)))
495 'cookies)\n') % (cookiefile, stringutil.forcebytestr(e)))
496
496
497 def http_request(self, request):
497 def http_request(self, request):
498 if self.cookiejar:
498 if self.cookiejar:
499 self.cookiejar.add_cookie_header(request)
499 self.cookiejar.add_cookie_header(request)
500
500
501 return request
501 return request
502
502
503 def https_request(self, request):
503 def https_request(self, request):
504 if self.cookiejar:
504 if self.cookiejar:
505 self.cookiejar.add_cookie_header(request)
505 self.cookiejar.add_cookie_header(request)
506
506
507 return request
507 return request
508
508
509 handlerfuncs = []
509 handlerfuncs = []
510
510
511 def opener(ui, authinfo=None, useragent=None, loggingfh=None,
511 def opener(ui, authinfo=None, useragent=None, loggingfh=None,
512 loggingname=b's', loggingopts=None, sendaccept=True):
512 loggingname=b's', loggingopts=None, sendaccept=True):
513 '''
513 '''
514 construct an opener suitable for urllib2
514 construct an opener suitable for urllib2
515 authinfo will be added to the password manager
515 authinfo will be added to the password manager
516
516
517 The opener can be configured to log socket events if the various
517 The opener can be configured to log socket events if the various
518 ``logging*`` arguments are specified.
518 ``logging*`` arguments are specified.
519
519
520 ``loggingfh`` denotes a file object to log events to.
520 ``loggingfh`` denotes a file object to log events to.
521 ``loggingname`` denotes the name of the to print when logging.
521 ``loggingname`` denotes the name of the to print when logging.
522 ``loggingopts`` is a dict of keyword arguments to pass to the constructed
522 ``loggingopts`` is a dict of keyword arguments to pass to the constructed
523 ``util.socketobserver`` instance.
523 ``util.socketobserver`` instance.
524
524
525 ``sendaccept`` allows controlling whether the ``Accept`` request header
525 ``sendaccept`` allows controlling whether the ``Accept`` request header
526 is sent. The header is sent by default.
526 is sent. The header is sent by default.
527 '''
527 '''
528 handlers = []
528 handlers = []
529
529
530 if loggingfh:
530 if loggingfh:
531 handlers.append(logginghttphandler(loggingfh, loggingname,
531 handlers.append(logginghttphandler(loggingfh, loggingname,
532 loggingopts or {}))
532 loggingopts or {}))
533 # We don't yet support HTTPS when logging I/O. If we attempt to open
533 # We don't yet support HTTPS when logging I/O. If we attempt to open
534 # an HTTPS URL, we'll likely fail due to unknown protocol.
534 # an HTTPS URL, we'll likely fail due to unknown protocol.
535
535
536 else:
536 else:
537 handlers.append(httphandler())
537 handlers.append(httphandler())
538 if has_https:
538 if has_https:
539 handlers.append(httpshandler(ui))
539 handlers.append(httpshandler(ui))
540
540
541 handlers.append(proxyhandler(ui))
541 handlers.append(proxyhandler(ui))
542
542
543 passmgr = passwordmgr(ui, ui.httppasswordmgrdb)
543 passmgr = passwordmgr(ui, ui.httppasswordmgrdb)
544 if authinfo is not None:
544 if authinfo is not None:
545 realm, uris, user, passwd = authinfo
545 realm, uris, user, passwd = authinfo
546 saveduser, savedpass = passmgr.find_stored_password(uris[0])
546 saveduser, savedpass = passmgr.find_stored_password(uris[0])
547 if user != saveduser or passwd:
547 if user != saveduser or passwd:
548 passmgr.add_password(realm, uris, user, passwd)
548 passmgr.add_password(realm, uris, user, passwd)
549 ui.debug('http auth: user %s, password %s\n' %
549 ui.debug('http auth: user %s, password %s\n' %
550 (user, passwd and '*' * len(passwd) or 'not set'))
550 (user, passwd and '*' * len(passwd) or 'not set'))
551
551
552 handlers.extend((httpbasicauthhandler(passmgr),
552 handlers.extend((httpbasicauthhandler(passmgr),
553 httpdigestauthhandler(passmgr)))
553 httpdigestauthhandler(passmgr)))
554 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
554 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
555 handlers.append(cookiehandler(ui))
555 handlers.append(cookiehandler(ui))
556 opener = urlreq.buildopener(*handlers)
556 opener = urlreq.buildopener(*handlers)
557
557
558 # The user agent should should *NOT* be used by servers for e.g.
558 # The user agent should should *NOT* be used by servers for e.g.
559 # protocol detection or feature negotiation: there are other
559 # protocol detection or feature negotiation: there are other
560 # facilities for that.
560 # facilities for that.
561 #
561 #
562 # "mercurial/proto-1.0" was the original user agent string and
562 # "mercurial/proto-1.0" was the original user agent string and
563 # exists for backwards compatibility reasons.
563 # exists for backwards compatibility reasons.
564 #
564 #
565 # The "(Mercurial %s)" string contains the distribution
565 # The "(Mercurial %s)" string contains the distribution
566 # name and version. Other client implementations should choose their
566 # name and version. Other client implementations should choose their
567 # own distribution name. Since servers should not be using the user
567 # own distribution name. Since servers should not be using the user
568 # agent string for anything, clients should be able to define whatever
568 # agent string for anything, clients should be able to define whatever
569 # user agent they deem appropriate.
569 # user agent they deem appropriate.
570 #
570 #
571 # The custom user agent is for lfs, because unfortunately some servers
571 # The custom user agent is for lfs, because unfortunately some servers
572 # do look at this value.
572 # do look at this value.
573 if not useragent:
573 if not useragent:
574 agent = 'mercurial/proto-1.0 (Mercurial %s)' % util.version()
574 agent = 'mercurial/proto-1.0 (Mercurial %s)' % util.version()
575 opener.addheaders = [(r'User-agent', pycompat.sysstr(agent))]
575 opener.addheaders = [(r'User-agent', pycompat.sysstr(agent))]
576 else:
576 else:
577 opener.addheaders = [(r'User-agent', pycompat.sysstr(useragent))]
577 opener.addheaders = [(r'User-agent', pycompat.sysstr(useragent))]
578
578
579 # This header should only be needed by wire protocol requests. But it has
579 # This header should only be needed by wire protocol requests. But it has
580 # been sent on all requests since forever. We keep sending it for backwards
580 # been sent on all requests since forever. We keep sending it for backwards
581 # compatibility reasons. Modern versions of the wire protocol use
581 # compatibility reasons. Modern versions of the wire protocol use
582 # X-HgProto-<N> for advertising client support.
582 # X-HgProto-<N> for advertising client support.
583 if sendaccept:
583 if sendaccept:
584 opener.addheaders.append((r'Accept', r'application/mercurial-0.1'))
584 opener.addheaders.append((r'Accept', r'application/mercurial-0.1'))
585
585
586 return opener
586 return opener
587
587
588 def open(ui, url_, data=None):
588 def open(ui, url_, data=None):
589 u = util.url(url_)
589 u = util.url(url_)
590 if u.scheme:
590 if u.scheme:
591 u.scheme = u.scheme.lower()
591 u.scheme = u.scheme.lower()
592 url_, authinfo = u.authinfo()
592 url_, authinfo = u.authinfo()
593 else:
593 else:
594 path = util.normpath(os.path.abspath(url_))
594 path = util.normpath(os.path.abspath(url_))
595 url_ = 'file://' + pycompat.bytesurl(urlreq.pathname2url(path))
595 url_ = 'file://' + pycompat.bytesurl(urlreq.pathname2url(path))
596 authinfo = None
596 authinfo = None
597 return opener(ui, authinfo).open(pycompat.strurl(url_), data)
597 return opener(ui, authinfo).open(pycompat.strurl(url_), data)
598
599 def wrapresponse(resp):
600 """Wrap a response object with common error handlers.
601
602 This ensures that any I/O from any consumer raises the appropriate
603 error and messaging.
604 """
605 origread = resp.read
606
607 class readerproxy(resp.__class__):
608 def read(self, size=None):
609 try:
610 return origread(size)
611 except httplib.IncompleteRead as e:
612 # e.expected is an integer if length known or None otherwise.
613 if e.expected:
614 got = len(e.partial)
615 total = e.expected + got
616 msg = _('HTTP request error (incomplete response; '
617 'expected %d bytes got %d)') % (total, got)
618 else:
619 msg = _('HTTP request error (incomplete response)')
620
621 raise error.PeerTransportError(
622 msg,
623 hint=_('this may be an intermittent network failure; '
624 'if the error persists, consider contacting the '
625 'network or server operator'))
626 except httplib.HTTPException as e:
627 raise error.PeerTransportError(
628 _('HTTP request error (%s)') % e,
629 hint=_('this may be an intermittent network failure; '
630 'if the error persists, consider contacting the '
631 'network or server operator'))
632
633 resp.__class__ = readerproxy
General Comments 0
You need to be logged in to leave comments. Login now