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