##// END OF EJS Templates
httppeer: always produce native str header keys and values...
Augie Fackler -
r34733:67e9678e default
parent child Browse files
Show More
@@ -1,464 +1,472
1 1 # httppeer.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import io
13 13 import os
14 14 import socket
15 15 import struct
16 16 import tempfile
17 17
18 18 from .i18n import _
19 19 from .node import nullid
20 20 from . import (
21 21 bundle2,
22 22 error,
23 23 httpconnection,
24 24 pycompat,
25 25 statichttprepo,
26 26 url,
27 27 util,
28 28 wireproto,
29 29 )
30 30
31 31 httplib = util.httplib
32 32 urlerr = util.urlerr
33 33 urlreq = util.urlreq
34 34
35 35 def encodevalueinheaders(value, header, limit):
36 36 """Encode a string value into multiple HTTP headers.
37 37
38 38 ``value`` will be encoded into 1 or more HTTP headers with the names
39 39 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
40 40 name + value will be at most ``limit`` bytes long.
41 41
42 Returns an iterable of 2-tuples consisting of header names and values.
42 Returns an iterable of 2-tuples consisting of header names and
43 values as native strings.
43 44 """
44 fmt = header + '-%s'
45 valuelen = limit - len(fmt % '000') - len(': \r\n')
45 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
46 # not bytes. This function always takes bytes in as arguments.
47 fmt = pycompat.strurl(header) + r'-%s'
48 # Note: it is *NOT* a bug that the last bit here is a bytestring
49 # and not a unicode: we're just getting the encoded length anyway,
50 # and using an r-string to make it portable between Python 2 and 3
51 # doesn't work because then the \r is a literal backslash-r
52 # instead of a carriage return.
53 valuelen = limit - len(fmt % r'000') - len(': \r\n')
46 54 result = []
47 55
48 56 n = 0
49 57 for i in xrange(0, len(value), valuelen):
50 58 n += 1
51 result.append((fmt % str(n), value[i:i + valuelen]))
59 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
52 60
53 61 return result
54 62
55 63 def _wraphttpresponse(resp):
56 64 """Wrap an HTTPResponse with common error handlers.
57 65
58 66 This ensures that any I/O from any consumer raises the appropriate
59 67 error and messaging.
60 68 """
61 69 origread = resp.read
62 70
63 71 class readerproxy(resp.__class__):
64 72 def read(self, size=None):
65 73 try:
66 74 return origread(size)
67 75 except httplib.IncompleteRead as e:
68 76 # e.expected is an integer if length known or None otherwise.
69 77 if e.expected:
70 78 msg = _('HTTP request error (incomplete response; '
71 79 'expected %d bytes got %d)') % (e.expected,
72 80 len(e.partial))
73 81 else:
74 82 msg = _('HTTP request error (incomplete response)')
75 83
76 84 raise error.PeerTransportError(
77 85 msg,
78 86 hint=_('this may be an intermittent network failure; '
79 87 'if the error persists, consider contacting the '
80 88 'network or server operator'))
81 89 except httplib.HTTPException as e:
82 90 raise error.PeerTransportError(
83 91 _('HTTP request error (%s)') % e,
84 92 hint=_('this may be an intermittent network failure; '
85 93 'if the error persists, consider contacting the '
86 94 'network or server operator'))
87 95
88 96 resp.__class__ = readerproxy
89 97
90 98 class _multifile(object):
91 99 def __init__(self, *fileobjs):
92 100 for f in fileobjs:
93 101 if not util.safehasattr(f, 'length'):
94 102 raise ValueError(
95 103 '_multifile only supports file objects that '
96 104 'have a length but this one does not:', type(f), f)
97 105 self._fileobjs = fileobjs
98 106 self._index = 0
99 107
100 108 @property
101 109 def length(self):
102 110 return sum(f.length for f in self._fileobjs)
103 111
104 112 def read(self, amt=None):
105 113 if amt <= 0:
106 114 return ''.join(f.read() for f in self._fileobjs)
107 115 parts = []
108 116 while amt and self._index < len(self._fileobjs):
109 117 parts.append(self._fileobjs[self._index].read(amt))
110 118 got = len(parts[-1])
111 119 if got < amt:
112 120 self._index += 1
113 121 amt -= got
114 122 return ''.join(parts)
115 123
116 124 def seek(self, offset, whence=os.SEEK_SET):
117 125 if whence != os.SEEK_SET:
118 126 raise NotImplementedError(
119 127 '_multifile does not support anything other'
120 128 ' than os.SEEK_SET for whence on seek()')
121 129 if offset != 0:
122 130 raise NotImplementedError(
123 131 '_multifile only supports seeking to start, but that '
124 132 'could be fixed if you need it')
125 133 for f in self._fileobjs:
126 134 f.seek(0)
127 135 self._index = 0
128 136
129 137 class httppeer(wireproto.wirepeer):
130 138 def __init__(self, ui, path):
131 139 self._path = path
132 140 self._caps = None
133 141 self._urlopener = None
134 142 self._requestbuilder = None
135 143 u = util.url(path)
136 144 if u.query or u.fragment:
137 145 raise error.Abort(_('unsupported URL component: "%s"') %
138 146 (u.query or u.fragment))
139 147
140 148 # urllib cannot handle URLs with embedded user or passwd
141 149 self._url, authinfo = u.authinfo()
142 150
143 151 self._ui = ui
144 152 ui.debug('using %s\n' % self._url)
145 153
146 154 self._urlopener = url.opener(ui, authinfo)
147 155 self._requestbuilder = urlreq.request
148 156
149 157 def __del__(self):
150 158 urlopener = getattr(self, '_urlopener', None)
151 159 if urlopener:
152 160 for h in urlopener.handlers:
153 161 h.close()
154 162 getattr(h, "close_all", lambda: None)()
155 163
156 164 # Begin of _basepeer interface.
157 165
158 166 @util.propertycache
159 167 def ui(self):
160 168 return self._ui
161 169
162 170 def url(self):
163 171 return self._path
164 172
165 173 def local(self):
166 174 return None
167 175
168 176 def peer(self):
169 177 return self
170 178
171 179 def canpush(self):
172 180 return True
173 181
174 182 def close(self):
175 183 pass
176 184
177 185 # End of _basepeer interface.
178 186
179 187 # Begin of _basewirepeer interface.
180 188
181 189 def capabilities(self):
182 190 if self._caps is None:
183 191 try:
184 192 self._fetchcaps()
185 193 except error.RepoError:
186 194 self._caps = set()
187 195 self.ui.debug('capabilities: %s\n' %
188 196 (' '.join(self._caps or ['none'])))
189 197 return self._caps
190 198
191 199 # End of _basewirepeer interface.
192 200
193 201 # look up capabilities only when needed
194 202
195 203 def _fetchcaps(self):
196 204 self._caps = set(self._call('capabilities').split())
197 205
198 206 def _callstream(self, cmd, _compressible=False, **args):
199 207 if cmd == 'pushkey':
200 208 args['data'] = ''
201 209 data = args.pop('data', None)
202 210 headers = args.pop('headers', {})
203 211
204 212 self.ui.debug("sending %s command\n" % cmd)
205 213 q = [('cmd', cmd)]
206 214 headersize = 0
207 215 varyheaders = []
208 216 # Important: don't use self.capable() here or else you end up
209 217 # with infinite recursion when trying to look up capabilities
210 218 # for the first time.
211 219 postargsok = self._caps is not None and 'httppostargs' in self._caps
212 220 if postargsok and args:
213 221 strargs = urlreq.urlencode(sorted(args.items()))
214 222 if not data:
215 223 data = strargs
216 224 else:
217 225 if isinstance(data, basestring):
218 226 i = io.BytesIO(data)
219 227 i.length = len(data)
220 228 data = i
221 229 argsio = io.BytesIO(strargs)
222 230 argsio.length = len(strargs)
223 231 data = _multifile(argsio, data)
224 232 headers[r'X-HgArgs-Post'] = len(strargs)
225 233 else:
226 234 if len(args) > 0:
227 235 httpheader = self.capable('httpheader')
228 236 if httpheader:
229 237 headersize = int(httpheader.split(',', 1)[0])
230 238 if headersize > 0:
231 239 # The headers can typically carry more data than the URL.
232 240 encargs = urlreq.urlencode(sorted(args.items()))
233 241 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
234 242 headersize):
235 243 headers[header] = value
236 244 varyheaders.append(header)
237 245 else:
238 246 q += sorted(args.items())
239 247 qs = '?%s' % urlreq.urlencode(q)
240 248 cu = "%s%s" % (self._url, qs)
241 249 size = 0
242 250 if util.safehasattr(data, 'length'):
243 251 size = data.length
244 252 elif data is not None:
245 253 size = len(data)
246 254 if size and self.ui.configbool('ui', 'usehttp2'):
247 255 headers[r'Expect'] = r'100-Continue'
248 256 headers[r'X-HgHttp2'] = r'1'
249 257 if data is not None and r'Content-Type' not in headers:
250 258 headers[r'Content-Type'] = r'application/mercurial-0.1'
251 259
252 260 # Tell the server we accept application/mercurial-0.2 and multiple
253 261 # compression formats if the server is capable of emitting those
254 262 # payloads.
255 263 protoparams = []
256 264
257 265 mediatypes = set()
258 266 if self._caps is not None:
259 267 mt = self.capable('httpmediatype')
260 268 if mt:
261 269 protoparams.append('0.1')
262 270 mediatypes = set(mt.split(','))
263 271
264 272 if '0.2tx' in mediatypes:
265 273 protoparams.append('0.2')
266 274
267 275 if '0.2tx' in mediatypes and self.capable('compression'):
268 276 # We /could/ compare supported compression formats and prune
269 277 # non-mutually supported or error if nothing is mutually supported.
270 278 # For now, send the full list to the server and have it error.
271 279 comps = [e.wireprotosupport().name for e in
272 280 util.compengines.supportedwireengines(util.CLIENTROLE)]
273 281 protoparams.append('comp=%s' % ','.join(comps))
274 282
275 283 if protoparams:
276 284 protoheaders = encodevalueinheaders(' '.join(protoparams),
277 285 'X-HgProto',
278 286 headersize or 1024)
279 287 for header, value in protoheaders:
280 288 headers[header] = value
281 289 varyheaders.append(header)
282 290
283 291 if varyheaders:
284 292 headers[r'Vary'] = r','.join(varyheaders)
285 293
286 294 req = self._requestbuilder(pycompat.strurl(cu), data, headers)
287 295
288 296 if data is not None:
289 297 self.ui.debug("sending %s bytes\n" % size)
290 298 req.add_unredirected_header('Content-Length', '%d' % size)
291 299 try:
292 300 resp = self._urlopener.open(req)
293 301 except urlerr.httperror as inst:
294 302 if inst.code == 401:
295 303 raise error.Abort(_('authorization failed'))
296 304 raise
297 305 except httplib.HTTPException as inst:
298 306 self.ui.debug('http error while sending %s command\n' % cmd)
299 307 self.ui.traceback()
300 308 raise IOError(None, inst)
301 309
302 310 # Insert error handlers for common I/O failures.
303 311 _wraphttpresponse(resp)
304 312
305 313 # record the url we got redirected to
306 314 resp_url = pycompat.bytesurl(resp.geturl())
307 315 if resp_url.endswith(qs):
308 316 resp_url = resp_url[:-len(qs)]
309 317 if self._url.rstrip('/') != resp_url.rstrip('/'):
310 318 if not self.ui.quiet:
311 319 self.ui.warn(_('real URL is %s\n') % resp_url)
312 320 self._url = resp_url
313 321 try:
314 322 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
315 323 except AttributeError:
316 324 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
317 325
318 326 safeurl = util.hidepassword(self._url)
319 327 if proto.startswith('application/hg-error'):
320 328 raise error.OutOfBandError(resp.read())
321 329 # accept old "text/plain" and "application/hg-changegroup" for now
322 330 if not (proto.startswith('application/mercurial-') or
323 331 (proto.startswith('text/plain')
324 332 and not resp.headers.get('content-length')) or
325 333 proto.startswith('application/hg-changegroup')):
326 334 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
327 335 raise error.RepoError(
328 336 _("'%s' does not appear to be an hg repository:\n"
329 337 "---%%<--- (%s)\n%s\n---%%<---\n")
330 338 % (safeurl, proto or 'no content-type', resp.read(1024)))
331 339
332 340 if proto.startswith('application/mercurial-'):
333 341 try:
334 342 version = proto.split('-', 1)[1]
335 343 version_info = tuple([int(n) for n in version.split('.')])
336 344 except ValueError:
337 345 raise error.RepoError(_("'%s' sent a broken Content-Type "
338 346 "header (%s)") % (safeurl, proto))
339 347
340 348 # TODO consider switching to a decompression reader that uses
341 349 # generators.
342 350 if version_info == (0, 1):
343 351 if _compressible:
344 352 return util.compengines['zlib'].decompressorreader(resp)
345 353 return resp
346 354 elif version_info == (0, 2):
347 355 # application/mercurial-0.2 always identifies the compression
348 356 # engine in the payload header.
349 357 elen = struct.unpack('B', resp.read(1))[0]
350 358 ename = resp.read(elen)
351 359 engine = util.compengines.forwiretype(ename)
352 360 return engine.decompressorreader(resp)
353 361 else:
354 362 raise error.RepoError(_("'%s' uses newer protocol %s") %
355 363 (safeurl, version))
356 364
357 365 if _compressible:
358 366 return util.compengines['zlib'].decompressorreader(resp)
359 367
360 368 return resp
361 369
362 370 def _call(self, cmd, **args):
363 371 fp = self._callstream(cmd, **args)
364 372 try:
365 373 return fp.read()
366 374 finally:
367 375 # if using keepalive, allow connection to be reused
368 376 fp.close()
369 377
370 378 def _callpush(self, cmd, cg, **args):
371 379 # have to stream bundle to a temp file because we do not have
372 380 # http 1.1 chunked transfer.
373 381
374 382 types = self.capable('unbundle')
375 383 try:
376 384 types = types.split(',')
377 385 except AttributeError:
378 386 # servers older than d1b16a746db6 will send 'unbundle' as a
379 387 # boolean capability. They only support headerless/uncompressed
380 388 # bundles.
381 389 types = [""]
382 390 for x in types:
383 391 if x in bundle2.bundletypes:
384 392 type = x
385 393 break
386 394
387 395 tempname = bundle2.writebundle(self.ui, cg, None, type)
388 396 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
389 397 headers = {'Content-Type': 'application/mercurial-0.1'}
390 398
391 399 try:
392 400 r = self._call(cmd, data=fp, headers=headers, **args)
393 401 vals = r.split('\n', 1)
394 402 if len(vals) < 2:
395 403 raise error.ResponseError(_("unexpected response:"), r)
396 404 return vals
397 405 except socket.error as err:
398 406 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
399 407 raise error.Abort(_('push failed: %s') % err.args[1])
400 408 raise error.Abort(err.args[1])
401 409 finally:
402 410 fp.close()
403 411 os.unlink(tempname)
404 412
405 413 def _calltwowaystream(self, cmd, fp, **args):
406 414 fh = None
407 415 fp_ = None
408 416 filename = None
409 417 try:
410 418 # dump bundle to disk
411 419 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
412 420 fh = os.fdopen(fd, pycompat.sysstr("wb"))
413 421 d = fp.read(4096)
414 422 while d:
415 423 fh.write(d)
416 424 d = fp.read(4096)
417 425 fh.close()
418 426 # start http push
419 427 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
420 428 headers = {'Content-Type': 'application/mercurial-0.1'}
421 429 return self._callstream(cmd, data=fp_, headers=headers, **args)
422 430 finally:
423 431 if fp_ is not None:
424 432 fp_.close()
425 433 if fh is not None:
426 434 fh.close()
427 435 os.unlink(filename)
428 436
429 437 def _callcompressable(self, cmd, **args):
430 438 return self._callstream(cmd, _compressible=True, **args)
431 439
432 440 def _abort(self, exception):
433 441 raise exception
434 442
435 443 class httpspeer(httppeer):
436 444 def __init__(self, ui, path):
437 445 if not url.has_https:
438 446 raise error.Abort(_('Python support for SSL and HTTPS '
439 447 'is not installed'))
440 448 httppeer.__init__(self, ui, path)
441 449
442 450 def instance(ui, path, create):
443 451 if create:
444 452 raise error.Abort(_('cannot create new http repository'))
445 453 try:
446 454 if path.startswith('https:'):
447 455 inst = httpspeer(ui, path)
448 456 else:
449 457 inst = httppeer(ui, path)
450 458 try:
451 459 # Try to do useful work when checking compatibility.
452 460 # Usually saves a roundtrip since we want the caps anyway.
453 461 inst._fetchcaps()
454 462 except error.RepoError:
455 463 # No luck, try older compatibility check.
456 464 inst.between([(nullid, nullid)])
457 465 return inst
458 466 except error.RepoError as httpexception:
459 467 try:
460 468 r = statichttprepo.instance(ui, "static-" + path, create)
461 469 ui.note(_('(falling back to static-http)\n'))
462 470 return r
463 471 except error.RepoError:
464 472 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now