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