##// END OF EJS Templates
httppeer: detect redirect to URL without query string (issue5860)...
Gregory Szorc -
r37851:6169d95d @24 stable
parent child Browse files
Show More
@@ -1,961 +1,996
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 import weakref
18 18
19 19 from .i18n import _
20 20 from .thirdparty import (
21 21 cbor,
22 22 )
23 23 from . import (
24 24 bundle2,
25 25 error,
26 26 httpconnection,
27 27 pycompat,
28 28 repository,
29 29 statichttprepo,
30 30 url as urlmod,
31 31 util,
32 32 wireprotoframing,
33 33 wireprototypes,
34 34 wireprotov1peer,
35 35 wireprotov2peer,
36 36 wireprotov2server,
37 37 )
38 38 from .utils import (
39 39 interfaceutil,
40 40 )
41 41
42 42 httplib = util.httplib
43 43 urlerr = util.urlerr
44 44 urlreq = util.urlreq
45 45
46 46 def encodevalueinheaders(value, header, limit):
47 47 """Encode a string value into multiple HTTP headers.
48 48
49 49 ``value`` will be encoded into 1 or more HTTP headers with the names
50 50 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
51 51 name + value will be at most ``limit`` bytes long.
52 52
53 53 Returns an iterable of 2-tuples consisting of header names and
54 54 values as native strings.
55 55 """
56 56 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
57 57 # not bytes. This function always takes bytes in as arguments.
58 58 fmt = pycompat.strurl(header) + r'-%s'
59 59 # Note: it is *NOT* a bug that the last bit here is a bytestring
60 60 # and not a unicode: we're just getting the encoded length anyway,
61 61 # and using an r-string to make it portable between Python 2 and 3
62 62 # doesn't work because then the \r is a literal backslash-r
63 63 # instead of a carriage return.
64 64 valuelen = limit - len(fmt % r'000') - len(': \r\n')
65 65 result = []
66 66
67 67 n = 0
68 68 for i in xrange(0, len(value), valuelen):
69 69 n += 1
70 70 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
71 71
72 72 return result
73 73
74 74 def _wraphttpresponse(resp):
75 75 """Wrap an HTTPResponse with common error handlers.
76 76
77 77 This ensures that any I/O from any consumer raises the appropriate
78 78 error and messaging.
79 79 """
80 80 origread = resp.read
81 81
82 82 class readerproxy(resp.__class__):
83 83 def read(self, size=None):
84 84 try:
85 85 return origread(size)
86 86 except httplib.IncompleteRead as e:
87 87 # e.expected is an integer if length known or None otherwise.
88 88 if e.expected:
89 89 msg = _('HTTP request error (incomplete response; '
90 90 'expected %d bytes got %d)') % (e.expected,
91 91 len(e.partial))
92 92 else:
93 93 msg = _('HTTP request error (incomplete response)')
94 94
95 95 raise error.PeerTransportError(
96 96 msg,
97 97 hint=_('this may be an intermittent network failure; '
98 98 'if the error persists, consider contacting the '
99 99 'network or server operator'))
100 100 except httplib.HTTPException as e:
101 101 raise error.PeerTransportError(
102 102 _('HTTP request error (%s)') % e,
103 103 hint=_('this may be an intermittent network failure; '
104 104 'if the error persists, consider contacting the '
105 105 'network or server operator'))
106 106
107 107 resp.__class__ = readerproxy
108 108
109 109 class _multifile(object):
110 110 def __init__(self, *fileobjs):
111 111 for f in fileobjs:
112 112 if not util.safehasattr(f, 'length'):
113 113 raise ValueError(
114 114 '_multifile only supports file objects that '
115 115 'have a length but this one does not:', type(f), f)
116 116 self._fileobjs = fileobjs
117 117 self._index = 0
118 118
119 119 @property
120 120 def length(self):
121 121 return sum(f.length for f in self._fileobjs)
122 122
123 123 def read(self, amt=None):
124 124 if amt <= 0:
125 125 return ''.join(f.read() for f in self._fileobjs)
126 126 parts = []
127 127 while amt and self._index < len(self._fileobjs):
128 128 parts.append(self._fileobjs[self._index].read(amt))
129 129 got = len(parts[-1])
130 130 if got < amt:
131 131 self._index += 1
132 132 amt -= got
133 133 return ''.join(parts)
134 134
135 135 def seek(self, offset, whence=os.SEEK_SET):
136 136 if whence != os.SEEK_SET:
137 137 raise NotImplementedError(
138 138 '_multifile does not support anything other'
139 139 ' than os.SEEK_SET for whence on seek()')
140 140 if offset != 0:
141 141 raise NotImplementedError(
142 142 '_multifile only supports seeking to start, but that '
143 143 'could be fixed if you need it')
144 144 for f in self._fileobjs:
145 145 f.seek(0)
146 146 self._index = 0
147 147
148 148 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
149 149 repobaseurl, cmd, args):
150 150 """Make an HTTP request to run a command for a version 1 client.
151 151
152 152 ``caps`` is a set of known server capabilities. The value may be
153 153 None if capabilities are not yet known.
154 154
155 155 ``capablefn`` is a function to evaluate a capability.
156 156
157 157 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
158 158 raw data to pass to it.
159 159 """
160 160 if cmd == 'pushkey':
161 161 args['data'] = ''
162 162 data = args.pop('data', None)
163 163 headers = args.pop('headers', {})
164 164
165 165 ui.debug("sending %s command\n" % cmd)
166 166 q = [('cmd', cmd)]
167 167 headersize = 0
168 168 # Important: don't use self.capable() here or else you end up
169 169 # with infinite recursion when trying to look up capabilities
170 170 # for the first time.
171 171 postargsok = caps is not None and 'httppostargs' in caps
172 172
173 173 # Send arguments via POST.
174 174 if postargsok and args:
175 175 strargs = urlreq.urlencode(sorted(args.items()))
176 176 if not data:
177 177 data = strargs
178 178 else:
179 179 if isinstance(data, bytes):
180 180 i = io.BytesIO(data)
181 181 i.length = len(data)
182 182 data = i
183 183 argsio = io.BytesIO(strargs)
184 184 argsio.length = len(strargs)
185 185 data = _multifile(argsio, data)
186 186 headers[r'X-HgArgs-Post'] = len(strargs)
187 187 elif args:
188 188 # Calling self.capable() can infinite loop if we are calling
189 189 # "capabilities". But that command should never accept wire
190 190 # protocol arguments. So this should never happen.
191 191 assert cmd != 'capabilities'
192 192 httpheader = capablefn('httpheader')
193 193 if httpheader:
194 194 headersize = int(httpheader.split(',', 1)[0])
195 195
196 196 # Send arguments via HTTP headers.
197 197 if headersize > 0:
198 198 # The headers can typically carry more data than the URL.
199 199 encargs = urlreq.urlencode(sorted(args.items()))
200 200 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
201 201 headersize):
202 202 headers[header] = value
203 203 # Send arguments via query string (Mercurial <1.9).
204 204 else:
205 205 q += sorted(args.items())
206 206
207 207 qs = '?%s' % urlreq.urlencode(q)
208 208 cu = "%s%s" % (repobaseurl, qs)
209 209 size = 0
210 210 if util.safehasattr(data, 'length'):
211 211 size = data.length
212 212 elif data is not None:
213 213 size = len(data)
214 214 if data is not None and r'Content-Type' not in headers:
215 215 headers[r'Content-Type'] = r'application/mercurial-0.1'
216 216
217 217 # Tell the server we accept application/mercurial-0.2 and multiple
218 218 # compression formats if the server is capable of emitting those
219 219 # payloads.
220 220 # Note: Keep this set empty by default, as client advertisement of
221 221 # protocol parameters should only occur after the handshake.
222 222 protoparams = set()
223 223
224 224 mediatypes = set()
225 225 if caps is not None:
226 226 mt = capablefn('httpmediatype')
227 227 if mt:
228 228 protoparams.add('0.1')
229 229 mediatypes = set(mt.split(','))
230 230
231 231 protoparams.add('partial-pull')
232 232
233 233 if '0.2tx' in mediatypes:
234 234 protoparams.add('0.2')
235 235
236 236 if '0.2tx' in mediatypes and capablefn('compression'):
237 237 # We /could/ compare supported compression formats and prune
238 238 # non-mutually supported or error if nothing is mutually supported.
239 239 # For now, send the full list to the server and have it error.
240 240 comps = [e.wireprotosupport().name for e in
241 241 util.compengines.supportedwireengines(util.CLIENTROLE)]
242 242 protoparams.add('comp=%s' % ','.join(comps))
243 243
244 244 if protoparams:
245 245 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
246 246 'X-HgProto',
247 247 headersize or 1024)
248 248 for header, value in protoheaders:
249 249 headers[header] = value
250 250
251 251 varyheaders = []
252 252 for header in headers:
253 253 if header.lower().startswith(r'x-hg'):
254 254 varyheaders.append(header)
255 255
256 256 if varyheaders:
257 257 headers[r'Vary'] = r','.join(sorted(varyheaders))
258 258
259 259 req = requestbuilder(pycompat.strurl(cu), data, headers)
260 260
261 261 if data is not None:
262 262 ui.debug("sending %d bytes\n" % size)
263 263 req.add_unredirected_header(r'Content-Length', r'%d' % size)
264 264
265 265 return req, cu, qs
266 266
267 267 def _reqdata(req):
268 268 """Get request data, if any. If no data, returns None."""
269 269 if pycompat.ispy3:
270 270 return req.data
271 271 if not req.has_data():
272 272 return None
273 273 return req.get_data()
274 274
275 275 def sendrequest(ui, opener, req):
276 276 """Send a prepared HTTP request.
277 277
278 278 Returns the response object.
279 279 """
280 280 if (ui.debugflag
281 281 and ui.configbool('devel', 'debug.peer-request')):
282 282 dbg = ui.debug
283 283 line = 'devel-peer-request: %s\n'
284 284 dbg(line % '%s %s' % (pycompat.bytesurl(req.get_method()),
285 285 pycompat.bytesurl(req.get_full_url())))
286 286 hgargssize = None
287 287
288 288 for header, value in sorted(req.header_items()):
289 289 header = pycompat.bytesurl(header)
290 290 value = pycompat.bytesurl(value)
291 291 if header.startswith('X-hgarg-'):
292 292 if hgargssize is None:
293 293 hgargssize = 0
294 294 hgargssize += len(value)
295 295 else:
296 296 dbg(line % ' %s %s' % (header, value))
297 297
298 298 if hgargssize is not None:
299 299 dbg(line % ' %d bytes of commands arguments in headers'
300 300 % hgargssize)
301 301 data = _reqdata(req)
302 302 if data is not None:
303 303 length = getattr(data, 'length', None)
304 304 if length is None:
305 305 length = len(data)
306 306 dbg(line % ' %d bytes of data' % length)
307 307
308 308 start = util.timer()
309 309
310 310 try:
311 311 res = opener.open(req)
312 312 except urlerr.httperror as inst:
313 313 if inst.code == 401:
314 314 raise error.Abort(_('authorization failed'))
315 315 raise
316 316 except httplib.HTTPException as inst:
317 317 ui.debug('http error requesting %s\n' %
318 318 util.hidepassword(req.get_full_url()))
319 319 ui.traceback()
320 320 raise IOError(None, inst)
321 321 finally:
322 322 if ui.configbool('devel', 'debug.peer-request'):
323 323 dbg(line % ' finished in %.4f seconds (%d)'
324 324 % (util.timer() - start, res.code))
325 325
326 326 # Insert error handlers for common I/O failures.
327 327 _wraphttpresponse(res)
328 328
329 329 return res
330 330
331 class RedirectedRepoError(error.RepoError):
332 def __init__(self, msg, respurl):
333 super(RedirectedRepoError, self).__init__(msg)
334 self.respurl = respurl
335
331 336 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
332 337 allowcbor=False):
333 338 # record the url we got redirected to
339 redirected = False
334 340 respurl = pycompat.bytesurl(resp.geturl())
335 341 if respurl.endswith(qs):
336 342 respurl = respurl[:-len(qs)]
343 qsdropped = False
344 else:
345 qsdropped = True
346
337 347 if baseurl.rstrip('/') != respurl.rstrip('/'):
348 redirected = True
338 349 if not ui.quiet:
339 350 ui.warn(_('real URL is %s\n') % respurl)
340 351
341 352 try:
342 353 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
343 354 except AttributeError:
344 355 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
345 356
346 357 safeurl = util.hidepassword(baseurl)
347 358 if proto.startswith('application/hg-error'):
348 359 raise error.OutOfBandError(resp.read())
349 360
350 361 # Pre 1.0 versions of Mercurial used text/plain and
351 362 # application/hg-changegroup. We don't support such old servers.
352 363 if not proto.startswith('application/mercurial-'):
353 364 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
354 raise error.RepoError(
355 _("'%s' does not appear to be an hg repository:\n"
356 "---%%<--- (%s)\n%s\n---%%<---\n")
357 % (safeurl, proto or 'no content-type', resp.read(1024)))
365 msg = _("'%s' does not appear to be an hg repository:\n"
366 "---%%<--- (%s)\n%s\n---%%<---\n") % (
367 safeurl, proto or 'no content-type', resp.read(1024))
368
369 # Some servers may strip the query string from the redirect. We
370 # raise a special error type so callers can react to this specially.
371 if redirected and qsdropped:
372 raise RedirectedRepoError(msg, respurl)
373 else:
374 raise error.RepoError(msg)
358 375
359 376 try:
360 377 subtype = proto.split('-', 1)[1]
361 378
362 379 # Unless we end up supporting CBOR in the legacy wire protocol,
363 380 # this should ONLY be encountered for the initial capabilities
364 381 # request during handshake.
365 382 if subtype == 'cbor':
366 383 if allowcbor:
367 384 return respurl, proto, resp
368 385 else:
369 386 raise error.RepoError(_('unexpected CBOR response from '
370 387 'server'))
371 388
372 389 version_info = tuple([int(n) for n in subtype.split('.')])
373 390 except ValueError:
374 391 raise error.RepoError(_("'%s' sent a broken Content-Type "
375 392 "header (%s)") % (safeurl, proto))
376 393
377 394 # TODO consider switching to a decompression reader that uses
378 395 # generators.
379 396 if version_info == (0, 1):
380 397 if compressible:
381 398 resp = util.compengines['zlib'].decompressorreader(resp)
382 399
383 400 elif version_info == (0, 2):
384 401 # application/mercurial-0.2 always identifies the compression
385 402 # engine in the payload header.
386 403 elen = struct.unpack('B', resp.read(1))[0]
387 404 ename = resp.read(elen)
388 405 engine = util.compengines.forwiretype(ename)
389 406
390 407 resp = engine.decompressorreader(resp)
391 408 else:
392 409 raise error.RepoError(_("'%s' uses newer protocol %s") %
393 410 (safeurl, subtype))
394 411
395 412 return respurl, proto, resp
396 413
397 414 class httppeer(wireprotov1peer.wirepeer):
398 415 def __init__(self, ui, path, url, opener, requestbuilder, caps):
399 416 self.ui = ui
400 417 self._path = path
401 418 self._url = url
402 419 self._caps = caps
403 420 self._urlopener = opener
404 421 self._requestbuilder = requestbuilder
405 422
406 423 def __del__(self):
407 424 for h in self._urlopener.handlers:
408 425 h.close()
409 426 getattr(h, "close_all", lambda: None)()
410 427
411 428 # Begin of ipeerconnection interface.
412 429
413 430 def url(self):
414 431 return self._path
415 432
416 433 def local(self):
417 434 return None
418 435
419 436 def peer(self):
420 437 return self
421 438
422 439 def canpush(self):
423 440 return True
424 441
425 442 def close(self):
426 443 pass
427 444
428 445 # End of ipeerconnection interface.
429 446
430 447 # Begin of ipeercommands interface.
431 448
432 449 def capabilities(self):
433 450 return self._caps
434 451
435 452 # End of ipeercommands interface.
436 453
437 # look up capabilities only when needed
438
439 454 def _callstream(self, cmd, _compressible=False, **args):
440 455 args = pycompat.byteskwargs(args)
441 456
442 457 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
443 458 self._caps, self.capable,
444 459 self._url, cmd, args)
445 460
446 461 resp = sendrequest(self.ui, self._urlopener, req)
447 462
448 463 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
449 464 resp, _compressible)
450 465
451 466 return resp
452 467
453 468 def _call(self, cmd, **args):
454 469 fp = self._callstream(cmd, **args)
455 470 try:
456 471 return fp.read()
457 472 finally:
458 473 # if using keepalive, allow connection to be reused
459 474 fp.close()
460 475
461 476 def _callpush(self, cmd, cg, **args):
462 477 # have to stream bundle to a temp file because we do not have
463 478 # http 1.1 chunked transfer.
464 479
465 480 types = self.capable('unbundle')
466 481 try:
467 482 types = types.split(',')
468 483 except AttributeError:
469 484 # servers older than d1b16a746db6 will send 'unbundle' as a
470 485 # boolean capability. They only support headerless/uncompressed
471 486 # bundles.
472 487 types = [""]
473 488 for x in types:
474 489 if x in bundle2.bundletypes:
475 490 type = x
476 491 break
477 492
478 493 tempname = bundle2.writebundle(self.ui, cg, None, type)
479 494 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
480 495 headers = {r'Content-Type': r'application/mercurial-0.1'}
481 496
482 497 try:
483 498 r = self._call(cmd, data=fp, headers=headers, **args)
484 499 vals = r.split('\n', 1)
485 500 if len(vals) < 2:
486 501 raise error.ResponseError(_("unexpected response:"), r)
487 502 return vals
488 503 except urlerr.httperror:
489 504 # Catch and re-raise these so we don't try and treat them
490 505 # like generic socket errors. They lack any values in
491 506 # .args on Python 3 which breaks our socket.error block.
492 507 raise
493 508 except socket.error as err:
494 509 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
495 510 raise error.Abort(_('push failed: %s') % err.args[1])
496 511 raise error.Abort(err.args[1])
497 512 finally:
498 513 fp.close()
499 514 os.unlink(tempname)
500 515
501 516 def _calltwowaystream(self, cmd, fp, **args):
502 517 fh = None
503 518 fp_ = None
504 519 filename = None
505 520 try:
506 521 # dump bundle to disk
507 522 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
508 523 fh = os.fdopen(fd, r"wb")
509 524 d = fp.read(4096)
510 525 while d:
511 526 fh.write(d)
512 527 d = fp.read(4096)
513 528 fh.close()
514 529 # start http push
515 530 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
516 531 headers = {r'Content-Type': r'application/mercurial-0.1'}
517 532 return self._callstream(cmd, data=fp_, headers=headers, **args)
518 533 finally:
519 534 if fp_ is not None:
520 535 fp_.close()
521 536 if fh is not None:
522 537 fh.close()
523 538 os.unlink(filename)
524 539
525 540 def _callcompressable(self, cmd, **args):
526 541 return self._callstream(cmd, _compressible=True, **args)
527 542
528 543 def _abort(self, exception):
529 544 raise exception
530 545
531 546 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests):
532 547 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
533 548 buffersends=True)
534 549
535 550 handler = wireprotov2peer.clienthandler(ui, reactor)
536 551
537 552 url = '%s/%s' % (apiurl, permission)
538 553
539 554 if len(requests) > 1:
540 555 url += '/multirequest'
541 556 else:
542 557 url += '/%s' % requests[0][0]
543 558
544 559 for command, args, f in requests:
545 560 assert not list(handler.callcommand(command, args, f))
546 561
547 562 # TODO stream this.
548 563 body = b''.join(map(bytes, handler.flushcommands()))
549 564
550 565 # TODO modify user-agent to reflect v2
551 566 headers = {
552 567 r'Accept': wireprotov2server.FRAMINGTYPE,
553 568 r'Content-Type': wireprotov2server.FRAMINGTYPE,
554 569 }
555 570
556 571 req = requestbuilder(pycompat.strurl(url), body, headers)
557 572 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
558 573
559 574 try:
560 575 res = opener.open(req)
561 576 except urlerr.httperror as e:
562 577 if e.code == 401:
563 578 raise error.Abort(_('authorization failed'))
564 579
565 580 raise
566 581 except httplib.HTTPException as e:
567 582 ui.traceback()
568 583 raise IOError(None, e)
569 584
570 585 return handler, res
571 586
572 587 class queuedcommandfuture(pycompat.futures.Future):
573 588 """Wraps result() on command futures to trigger submission on call."""
574 589
575 590 def result(self, timeout=None):
576 591 if self.done():
577 592 return pycompat.futures.Future.result(self, timeout)
578 593
579 594 self._peerexecutor.sendcommands()
580 595
581 596 # sendcommands() will restore the original __class__ and self.result
582 597 # will resolve to Future.result.
583 598 return self.result(timeout)
584 599
585 600 @interfaceutil.implementer(repository.ipeercommandexecutor)
586 601 class httpv2executor(object):
587 602 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor):
588 603 self._ui = ui
589 604 self._opener = opener
590 605 self._requestbuilder = requestbuilder
591 606 self._apiurl = apiurl
592 607 self._descriptor = descriptor
593 608 self._sent = False
594 609 self._closed = False
595 610 self._neededpermissions = set()
596 611 self._calls = []
597 612 self._futures = weakref.WeakSet()
598 613 self._responseexecutor = None
599 614 self._responsef = None
600 615
601 616 def __enter__(self):
602 617 return self
603 618
604 619 def __exit__(self, exctype, excvalue, exctb):
605 620 self.close()
606 621
607 622 def callcommand(self, command, args):
608 623 if self._sent:
609 624 raise error.ProgrammingError('callcommand() cannot be used after '
610 625 'commands are sent')
611 626
612 627 if self._closed:
613 628 raise error.ProgrammingError('callcommand() cannot be used after '
614 629 'close()')
615 630
616 631 # The service advertises which commands are available. So if we attempt
617 632 # to call an unknown command or pass an unknown argument, we can screen
618 633 # for this.
619 634 if command not in self._descriptor['commands']:
620 635 raise error.ProgrammingError(
621 636 'wire protocol command %s is not available' % command)
622 637
623 638 cmdinfo = self._descriptor['commands'][command]
624 639 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
625 640
626 641 if unknownargs:
627 642 raise error.ProgrammingError(
628 643 'wire protocol command %s does not accept argument: %s' % (
629 644 command, ', '.join(sorted(unknownargs))))
630 645
631 646 self._neededpermissions |= set(cmdinfo['permissions'])
632 647
633 648 # TODO we /could/ also validate types here, since the API descriptor
634 649 # includes types...
635 650
636 651 f = pycompat.futures.Future()
637 652
638 653 # Monkeypatch it so result() triggers sendcommands(), otherwise result()
639 654 # could deadlock.
640 655 f.__class__ = queuedcommandfuture
641 656 f._peerexecutor = self
642 657
643 658 self._futures.add(f)
644 659 self._calls.append((command, args, f))
645 660
646 661 return f
647 662
648 663 def sendcommands(self):
649 664 if self._sent:
650 665 return
651 666
652 667 if not self._calls:
653 668 return
654 669
655 670 self._sent = True
656 671
657 672 # Unhack any future types so caller sees a clean type and so we
658 673 # break reference cycle.
659 674 for f in self._futures:
660 675 if isinstance(f, queuedcommandfuture):
661 676 f.__class__ = pycompat.futures.Future
662 677 f._peerexecutor = None
663 678
664 679 # Mark the future as running and filter out cancelled futures.
665 680 calls = [(command, args, f)
666 681 for command, args, f in self._calls
667 682 if f.set_running_or_notify_cancel()]
668 683
669 684 # Clear out references, prevent improper object usage.
670 685 self._calls = None
671 686
672 687 if not calls:
673 688 return
674 689
675 690 permissions = set(self._neededpermissions)
676 691
677 692 if 'push' in permissions and 'pull' in permissions:
678 693 permissions.remove('pull')
679 694
680 695 if len(permissions) > 1:
681 696 raise error.RepoError(_('cannot make request requiring multiple '
682 697 'permissions: %s') %
683 698 _(', ').join(sorted(permissions)))
684 699
685 700 permission = {
686 701 'push': 'rw',
687 702 'pull': 'ro',
688 703 }[permissions.pop()]
689 704
690 705 handler, resp = sendv2request(
691 706 self._ui, self._opener, self._requestbuilder, self._apiurl,
692 707 permission, calls)
693 708
694 709 # TODO we probably want to validate the HTTP code, media type, etc.
695 710
696 711 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
697 712 self._responsef = self._responseexecutor.submit(self._handleresponse,
698 713 handler, resp)
699 714
700 715 def close(self):
701 716 if self._closed:
702 717 return
703 718
704 719 self.sendcommands()
705 720
706 721 self._closed = True
707 722
708 723 if not self._responsef:
709 724 return
710 725
711 726 try:
712 727 self._responsef.result()
713 728 finally:
714 729 self._responseexecutor.shutdown(wait=True)
715 730 self._responsef = None
716 731 self._responseexecutor = None
717 732
718 733 # If any of our futures are still in progress, mark them as
719 734 # errored, otherwise a result() could wait indefinitely.
720 735 for f in self._futures:
721 736 if not f.done():
722 737 f.set_exception(error.ResponseError(
723 738 _('unfulfilled command response')))
724 739
725 740 self._futures = None
726 741
727 742 def _handleresponse(self, handler, resp):
728 743 # Called in a thread to read the response.
729 744
730 745 while handler.readframe(resp):
731 746 pass
732 747
733 748 # TODO implement interface for version 2 peers
734 749 @interfaceutil.implementer(repository.ipeerconnection,
735 750 repository.ipeercapabilities,
736 751 repository.ipeerrequests)
737 752 class httpv2peer(object):
738 753 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
739 754 apidescriptor):
740 755 self.ui = ui
741 756
742 757 if repourl.endswith('/'):
743 758 repourl = repourl[:-1]
744 759
745 760 self._url = repourl
746 761 self._apipath = apipath
747 762 self._apiurl = '%s/%s' % (repourl, apipath)
748 763 self._opener = opener
749 764 self._requestbuilder = requestbuilder
750 765 self._descriptor = apidescriptor
751 766
752 767 # Start of ipeerconnection.
753 768
754 769 def url(self):
755 770 return self._url
756 771
757 772 def local(self):
758 773 return None
759 774
760 775 def peer(self):
761 776 return self
762 777
763 778 def canpush(self):
764 779 # TODO change once implemented.
765 780 return False
766 781
767 782 def close(self):
768 783 pass
769 784
770 785 # End of ipeerconnection.
771 786
772 787 # Start of ipeercapabilities.
773 788
774 789 def capable(self, name):
775 790 # The capabilities used internally historically map to capabilities
776 791 # advertised from the "capabilities" wire protocol command. However,
777 792 # version 2 of that command works differently.
778 793
779 794 # Maps to commands that are available.
780 795 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
781 796 return True
782 797
783 798 # Other concepts.
784 799 if name in ('bundle2',):
785 800 return True
786 801
787 802 return False
788 803
789 804 def requirecap(self, name, purpose):
790 805 if self.capable(name):
791 806 return
792 807
793 808 raise error.CapabilityError(
794 809 _('cannot %s; client or remote repository does not support the %r '
795 810 'capability') % (purpose, name))
796 811
797 812 # End of ipeercapabilities.
798 813
799 814 def _call(self, name, **args):
800 815 with self.commandexecutor() as e:
801 816 return e.callcommand(name, args).result()
802 817
803 818 def commandexecutor(self):
804 819 return httpv2executor(self.ui, self._opener, self._requestbuilder,
805 820 self._apiurl, self._descriptor)
806 821
807 822 # Registry of API service names to metadata about peers that handle it.
808 823 #
809 824 # The following keys are meaningful:
810 825 #
811 826 # init
812 827 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
813 828 # apidescriptor) to create a peer.
814 829 #
815 830 # priority
816 831 # Integer priority for the service. If we could choose from multiple
817 832 # services, we choose the one with the highest priority.
818 833 API_PEERS = {
819 834 wireprototypes.HTTP_WIREPROTO_V2: {
820 835 'init': httpv2peer,
821 836 'priority': 50,
822 837 },
823 838 }
824 839
825 840 def performhandshake(ui, url, opener, requestbuilder):
826 841 # The handshake is a request to the capabilities command.
827 842
828 843 caps = None
829 844 def capable(x):
830 845 raise error.ProgrammingError('should not be called')
831 846
832 847 args = {}
833 848
834 849 # The client advertises support for newer protocols by adding an
835 850 # X-HgUpgrade-* header with a list of supported APIs and an
836 851 # X-HgProto-* header advertising which serializing formats it supports.
837 852 # We only support the HTTP version 2 transport and CBOR responses for
838 853 # now.
839 854 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
840 855
841 856 if advertisev2:
842 857 args['headers'] = {
843 858 r'X-HgProto-1': r'cbor',
844 859 }
845 860
846 861 args['headers'].update(
847 862 encodevalueinheaders(' '.join(sorted(API_PEERS)),
848 863 'X-HgUpgrade',
849 864 # We don't know the header limit this early.
850 865 # So make it small.
851 866 1024))
852 867
853 868 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
854 869 capable, url, 'capabilities',
855 870 args)
856
857 871 resp = sendrequest(ui, opener, req)
858 872
873 # The server may redirect us to the repo root, stripping the
874 # ?cmd=capabilities query string from the URL. The server would likely
875 # return HTML in this case and ``parsev1commandresponse()`` would raise.
876 # We catch this special case and re-issue the capabilities request against
877 # the new URL.
878 #
879 # We should ideally not do this, as a redirect that drops the query
880 # string from the URL is arguably a server bug. (Garbage in, garbage out).
881 # However, Mercurial clients for several years appeared to handle this
882 # issue without behavior degradation. And according to issue 5860, it may
883 # be a longstanding bug in some server implementations. So we allow a
884 # redirect that drops the query string to "just work."
885 try:
886 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
887 compressible=False,
888 allowcbor=advertisev2)
889 except RedirectedRepoError as e:
890 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
891 capable, e.respurl,
892 'capabilities', args)
893 resp = sendrequest(ui, opener, req)
859 894 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
860 895 compressible=False,
861 896 allowcbor=advertisev2)
862 897
863 898 try:
864 899 rawdata = resp.read()
865 900 finally:
866 901 resp.close()
867 902
868 903 if not ct.startswith('application/mercurial-'):
869 904 raise error.ProgrammingError('unexpected content-type: %s' % ct)
870 905
871 906 if advertisev2:
872 907 if ct == 'application/mercurial-cbor':
873 908 try:
874 909 info = cbor.loads(rawdata)
875 910 except cbor.CBORDecodeError:
876 911 raise error.Abort(_('error decoding CBOR from remote server'),
877 912 hint=_('try again and consider contacting '
878 913 'the server operator'))
879 914
880 915 # We got a legacy response. That's fine.
881 916 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
882 917 info = {
883 918 'v1capabilities': set(rawdata.split())
884 919 }
885 920
886 921 else:
887 922 raise error.RepoError(
888 923 _('unexpected response type from server: %s') % ct)
889 924 else:
890 925 info = {
891 926 'v1capabilities': set(rawdata.split())
892 927 }
893 928
894 929 return respurl, info
895 930
896 931 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
897 932 """Construct an appropriate HTTP peer instance.
898 933
899 934 ``opener`` is an ``url.opener`` that should be used to establish
900 935 connections, perform HTTP requests.
901 936
902 937 ``requestbuilder`` is the type used for constructing HTTP requests.
903 938 It exists as an argument so extensions can override the default.
904 939 """
905 940 u = util.url(path)
906 941 if u.query or u.fragment:
907 942 raise error.Abort(_('unsupported URL component: "%s"') %
908 943 (u.query or u.fragment))
909 944
910 945 # urllib cannot handle URLs with embedded user or passwd.
911 946 url, authinfo = u.authinfo()
912 947 ui.debug('using %s\n' % url)
913 948
914 949 opener = opener or urlmod.opener(ui, authinfo)
915 950
916 951 respurl, info = performhandshake(ui, url, opener, requestbuilder)
917 952
918 953 # Given the intersection of APIs that both we and the server support,
919 954 # sort by their advertised priority and pick the first one.
920 955 #
921 956 # TODO consider making this request-based and interface driven. For
922 957 # example, the caller could say "I want a peer that does X." It's quite
923 958 # possible that not all peers would do that. Since we know the service
924 959 # capabilities, we could filter out services not meeting the
925 960 # requirements. Possibly by consulting the interfaces defined by the
926 961 # peer type.
927 962 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
928 963
929 964 preferredchoices = sorted(apipeerchoices,
930 965 key=lambda x: API_PEERS[x]['priority'],
931 966 reverse=True)
932 967
933 968 for service in preferredchoices:
934 969 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
935 970
936 971 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
937 972 requestbuilder,
938 973 info['apis'][service])
939 974
940 975 # Failed to construct an API peer. Fall back to legacy.
941 976 return httppeer(ui, path, respurl, opener, requestbuilder,
942 977 info['v1capabilities'])
943 978
944 979 def instance(ui, path, create, intents=None):
945 980 if create:
946 981 raise error.Abort(_('cannot create new http repository'))
947 982 try:
948 983 if path.startswith('https:') and not urlmod.has_https:
949 984 raise error.Abort(_('Python support for SSL and HTTPS '
950 985 'is not installed'))
951 986
952 987 inst = makepeer(ui, path)
953 988
954 989 return inst
955 990 except error.RepoError as httpexception:
956 991 try:
957 992 r = statichttprepo.instance(ui, "static-" + path, create)
958 993 ui.note(_('(falling back to static-http)\n'))
959 994 return r
960 995 except error.RepoError:
961 996 raise httpexception # use the original http RepoError instead
@@ -1,335 +1,726
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ cat >> $HGRCPATH << EOF
4 4 > [web]
5 5 > push_ssl = false
6 6 > allow_push = *
7 7 > EOF
8 8
9 9 $ hg init server
10 10 $ cd server
11 11 $ touch a
12 12 $ hg -q commit -A -m initial
13 13 $ cd ..
14 14
15 15 $ hg serve -R server -p $HGPORT -d --pid-file hg.pid
16 16 $ cat hg.pid >> $DAEMON_PIDS
17 17
18 18 compression formats are advertised in compression capability
19 19
20 20 #if zstd
21 21 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=zstd,zlib$' > /dev/null
22 22 #else
23 23 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=zlib$' > /dev/null
24 24 #endif
25 25
26 26 $ killdaemons.py
27 27
28 28 server.compressionengines can replace engines list wholesale
29 29
30 30 $ hg serve --config server.compressionengines=none -R server -p $HGPORT -d --pid-file hg.pid
31 31 $ cat hg.pid > $DAEMON_PIDS
32 32 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=none$' > /dev/null
33 33
34 34 $ killdaemons.py
35 35
36 36 Order of engines can also change
37 37
38 38 $ hg serve --config server.compressionengines=none,zlib -R server -p $HGPORT -d --pid-file hg.pid
39 39 $ cat hg.pid > $DAEMON_PIDS
40 40 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities' | tr ' ' '\n' | grep '^compression=none,zlib$' > /dev/null
41 41
42 42 $ killdaemons.py
43 43
44 44 Start a default server again
45 45
46 46 $ hg serve -R server -p $HGPORT -d --pid-file hg.pid
47 47 $ cat hg.pid > $DAEMON_PIDS
48 48
49 49 Server should send application/mercurial-0.1 to clients if no Accept is used
50 50
51 51 $ get-with-headers.py --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
52 52 200 Script output follows
53 53 content-type: application/mercurial-0.1
54 54 date: $HTTP_DATE$
55 55 server: testing stub value
56 56 transfer-encoding: chunked
57 57
58 58 Server should send application/mercurial-0.1 when client says it wants it
59 59
60 60 $ get-with-headers.py --hgproto '0.1' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
61 61 200 Script output follows
62 62 content-type: application/mercurial-0.1
63 63 date: $HTTP_DATE$
64 64 server: testing stub value
65 65 transfer-encoding: chunked
66 66
67 67 Server should send application/mercurial-0.2 when client says it wants it
68 68
69 69 $ get-with-headers.py --hgproto '0.2' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
70 70 200 Script output follows
71 71 content-type: application/mercurial-0.2
72 72 date: $HTTP_DATE$
73 73 server: testing stub value
74 74 transfer-encoding: chunked
75 75
76 76 $ get-with-headers.py --hgproto '0.1 0.2' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
77 77 200 Script output follows
78 78 content-type: application/mercurial-0.2
79 79 date: $HTTP_DATE$
80 80 server: testing stub value
81 81 transfer-encoding: chunked
82 82
83 83 Requesting a compression format that server doesn't support results will fall back to 0.1
84 84
85 85 $ get-with-headers.py --hgproto '0.2 comp=aa' --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
86 86 200 Script output follows
87 87 content-type: application/mercurial-0.1
88 88 date: $HTTP_DATE$
89 89 server: testing stub value
90 90 transfer-encoding: chunked
91 91
92 92 #if zstd
93 93 zstd is used if available
94 94
95 95 $ get-with-headers.py --hgproto '0.2 comp=zstd' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
96 96 $ f --size --hexdump --bytes 36 --sha1 resp
97 97 resp: size=248, sha1=4d8d8f87fb82bd542ce52881fdc94f850748
98 98 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
99 99 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 7a 73 74 64 |t follows...zstd|
100 100 0020: 28 b5 2f fd |(./.|
101 101
102 102 #endif
103 103
104 104 application/mercurial-0.2 is not yet used on non-streaming responses
105 105
106 106 $ get-with-headers.py --hgproto '0.2' $LOCALIP:$HGPORT '?cmd=heads' -
107 107 200 Script output follows
108 108 content-length: 41
109 109 content-type: application/mercurial-0.1
110 110 date: $HTTP_DATE$
111 111 server: testing stub value
112 112
113 113 e93700bd72895c5addab234c56d4024b487a362f
114 114
115 115 Now test protocol preference usage
116 116
117 117 $ killdaemons.py
118 118 $ hg serve --config server.compressionengines=none,zlib -R server -p $HGPORT -d --pid-file hg.pid
119 119 $ cat hg.pid > $DAEMON_PIDS
120 120
121 121 No Accept will send 0.1+zlib, even though "none" is preferred b/c "none" isn't supported on 0.1
122 122
123 123 $ get-with-headers.py --headeronly $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' Content-Type
124 124 200 Script output follows
125 125 content-type: application/mercurial-0.1
126 126
127 127 $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
128 128 $ f --size --hexdump --bytes 28 --sha1 resp
129 129 resp: size=227, sha1=35a4c074da74f32f5440da3cbf04
130 130 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
131 131 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 78 |t follows..x|
132 132
133 133 Explicit 0.1 will send zlib because "none" isn't supported on 0.1
134 134
135 135 $ get-with-headers.py --hgproto '0.1' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
136 136 $ f --size --hexdump --bytes 28 --sha1 resp
137 137 resp: size=227, sha1=35a4c074da74f32f5440da3cbf04
138 138 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
139 139 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 78 |t follows..x|
140 140
141 141 0.2 with no compression will get "none" because that is server's preference
142 142 (spec says ZL and UN are implicitly supported)
143 143
144 144 $ get-with-headers.py --hgproto '0.2' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
145 145 $ f --size --hexdump --bytes 32 --sha1 resp
146 146 resp: size=432, sha1=ac931b412ec185a02e0e5bcff98dac83
147 147 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
148 148 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 6e 6f 6e 65 |t follows...none|
149 149
150 150 Client receives server preference even if local order doesn't match
151 151
152 152 $ get-with-headers.py --hgproto '0.2 comp=zlib,none' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
153 153 $ f --size --hexdump --bytes 32 --sha1 resp
154 154 resp: size=432, sha1=ac931b412ec185a02e0e5bcff98dac83
155 155 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
156 156 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 6e 6f 6e 65 |t follows...none|
157 157
158 158 Client receives only supported format even if not server preferred format
159 159
160 160 $ get-with-headers.py --hgproto '0.2 comp=zlib' $LOCALIP:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
161 161 $ f --size --hexdump --bytes 33 --sha1 resp
162 162 resp: size=232, sha1=a1c727f0c9693ca15742a75c30419bc36
163 163 0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
164 164 0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 04 7a 6c 69 62 |t follows...zlib|
165 165 0020: 78 |x|
166 166
167 167 $ killdaemons.py
168 168 $ cd ..
169 169
170 170 Test listkeys for listing namespaces
171 171
172 172 $ hg init empty
173 173 $ hg -R empty serve -p $HGPORT -d --pid-file hg.pid
174 174 $ cat hg.pid > $DAEMON_PIDS
175 175
176 176 $ hg --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF
177 177 > command listkeys
178 178 > namespace namespaces
179 179 > EOF
180 180 s> GET /?cmd=capabilities HTTP/1.1\r\n
181 181 s> Accept-Encoding: identity\r\n
182 182 s> accept: application/mercurial-0.1\r\n
183 183 s> host: $LOCALIP:$HGPORT\r\n (glob)
184 184 s> user-agent: Mercurial debugwireproto\r\n
185 185 s> \r\n
186 186 s> makefile('rb', None)
187 187 s> HTTP/1.1 200 Script output follows\r\n
188 188 s> Server: testing stub value\r\n
189 189 s> Date: $HTTP_DATE$\r\n
190 190 s> Content-Type: application/mercurial-0.1\r\n
191 191 s> Content-Length: *\r\n (glob)
192 192 s> \r\n
193 193 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
194 194 sending listkeys command
195 195 s> GET /?cmd=listkeys HTTP/1.1\r\n
196 196 s> Accept-Encoding: identity\r\n
197 197 s> vary: X-HgArg-1,X-HgProto-1\r\n
198 198 s> x-hgarg-1: namespace=namespaces\r\n
199 199 s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
200 200 s> accept: application/mercurial-0.1\r\n
201 201 s> host: $LOCALIP:$HGPORT\r\n (glob)
202 202 s> user-agent: Mercurial debugwireproto\r\n
203 203 s> \r\n
204 204 s> makefile('rb', None)
205 205 s> HTTP/1.1 200 Script output follows\r\n
206 206 s> Server: testing stub value\r\n
207 207 s> Date: $HTTP_DATE$\r\n
208 208 s> Content-Type: application/mercurial-0.1\r\n
209 209 s> Content-Length: 30\r\n
210 210 s> \r\n
211 211 s> bookmarks\t\n
212 212 s> namespaces\t\n
213 213 s> phases\t
214 214 response: {b'bookmarks': b'', b'namespaces': b'', b'phases': b''}
215 215
216 216 Same thing, but with "httprequest" command
217 217
218 218 $ hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT << EOF
219 219 > httprequest GET ?cmd=listkeys
220 220 > user-agent: test
221 221 > x-hgarg-1: namespace=namespaces
222 222 > EOF
223 223 using raw connection to peer
224 224 s> GET /?cmd=listkeys HTTP/1.1\r\n
225 225 s> Accept-Encoding: identity\r\n
226 226 s> user-agent: test\r\n
227 227 s> x-hgarg-1: namespace=namespaces\r\n
228 228 s> host: $LOCALIP:$HGPORT\r\n (glob)
229 229 s> \r\n
230 230 s> makefile('rb', None)
231 231 s> HTTP/1.1 200 Script output follows\r\n
232 232 s> Server: testing stub value\r\n
233 233 s> Date: $HTTP_DATE$\r\n
234 234 s> Content-Type: application/mercurial-0.1\r\n
235 235 s> Content-Length: 30\r\n
236 236 s> \r\n
237 237 s> bookmarks\t\n
238 238 s> namespaces\t\n
239 239 s> phases\t
240 240
241 241 Client with HTTPv2 enabled advertises that and gets old capabilities response from old server
242 242
243 243 $ hg --config experimental.httppeer.advertise-v2=true --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF
244 244 > command heads
245 245 > EOF
246 246 s> GET /?cmd=capabilities HTTP/1.1\r\n
247 247 s> Accept-Encoding: identity\r\n
248 248 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
249 249 s> x-hgproto-1: cbor\r\n
250 250 s> x-hgupgrade-1: exp-http-v2-0001\r\n
251 251 s> accept: application/mercurial-0.1\r\n
252 252 s> host: $LOCALIP:$HGPORT\r\n (glob)
253 253 s> user-agent: Mercurial debugwireproto\r\n
254 254 s> \r\n
255 255 s> makefile('rb', None)
256 256 s> HTTP/1.1 200 Script output follows\r\n
257 257 s> Server: testing stub value\r\n
258 258 s> Date: $HTTP_DATE$\r\n
259 259 s> Content-Type: application/mercurial-0.1\r\n
260 260 s> Content-Length: *\r\n (glob)
261 261 s> \r\n
262 262 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
263 263 sending heads command
264 264 s> GET /?cmd=heads HTTP/1.1\r\n
265 265 s> Accept-Encoding: identity\r\n
266 266 s> vary: X-HgProto-1\r\n
267 267 s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
268 268 s> accept: application/mercurial-0.1\r\n
269 269 s> host: $LOCALIP:$HGPORT\r\n (glob)
270 270 s> user-agent: Mercurial debugwireproto\r\n
271 271 s> \r\n
272 272 s> makefile('rb', None)
273 273 s> HTTP/1.1 200 Script output follows\r\n
274 274 s> Server: testing stub value\r\n
275 275 s> Date: $HTTP_DATE$\r\n
276 276 s> Content-Type: application/mercurial-0.1\r\n
277 277 s> Content-Length: 41\r\n
278 278 s> \r\n
279 279 s> 0000000000000000000000000000000000000000\n
280 280 response: [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']
281 281
282 282 $ killdaemons.py
283 283 $ enablehttpv2 empty
284 284 $ hg --config server.compressionengines=zlib -R empty serve -p $HGPORT -d --pid-file hg.pid
285 285 $ cat hg.pid > $DAEMON_PIDS
286 286
287 287 Client with HTTPv2 enabled automatically upgrades if the server supports it
288 288
289 289 $ hg --config experimental.httppeer.advertise-v2=true --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF
290 290 > command heads
291 291 > EOF
292 292 s> GET /?cmd=capabilities HTTP/1.1\r\n
293 293 s> Accept-Encoding: identity\r\n
294 294 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
295 295 s> x-hgproto-1: cbor\r\n
296 296 s> x-hgupgrade-1: exp-http-v2-0001\r\n
297 297 s> accept: application/mercurial-0.1\r\n
298 298 s> host: $LOCALIP:$HGPORT\r\n (glob)
299 299 s> user-agent: Mercurial debugwireproto\r\n
300 300 s> \r\n
301 301 s> makefile('rb', None)
302 302 s> HTTP/1.1 200 OK\r\n
303 303 s> Server: testing stub value\r\n
304 304 s> Date: $HTTP_DATE$\r\n
305 305 s> Content-Type: application/mercurial-cbor\r\n
306 306 s> Content-Length: *\r\n (glob)
307 307 s> \r\n
308 308 s> \xa3Dapis\xa1Pexp-http-v2-0001\xa4Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x81\xa1DnameDzlibNrawrepoformats\x82LgeneraldeltaHrevlogv1Qframingmediatypes\x81X&application/mercurial-exp-framing-0005GapibaseDapi/Nv1capabilitiesY\x01\xc5batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
309 309 sending heads command
310 310 s> POST /api/exp-http-v2-0001/ro/heads HTTP/1.1\r\n
311 311 s> Accept-Encoding: identity\r\n
312 312 s> accept: application/mercurial-exp-framing-0005\r\n
313 313 s> content-type: application/mercurial-exp-framing-0005\r\n
314 314 s> content-length: 20\r\n
315 315 s> host: $LOCALIP:$HGPORT\r\n (glob)
316 316 s> user-agent: Mercurial debugwireproto\r\n
317 317 s> \r\n
318 318 s> \x0c\x00\x00\x01\x00\x01\x01\x11\xa1DnameEheads
319 319 s> makefile('rb', None)
320 320 s> HTTP/1.1 200 OK\r\n
321 321 s> Server: testing stub value\r\n
322 322 s> Date: $HTTP_DATE$\r\n
323 323 s> Content-Type: application/mercurial-exp-framing-0005\r\n
324 324 s> Transfer-Encoding: chunked\r\n
325 325 s> \r\n
326 326 s> 29\r\n
327 327 s> !\x00\x00\x01\x00\x02\x012
328 328 s> \xa1FstatusBok\x81T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
329 329 s> \r\n
330 330 received frame(size=33; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=eos)
331 331 s> 0\r\n
332 332 s> \r\n
333 333 response: [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']
334 334
335 335 $ killdaemons.py
336
337 HTTP client follows HTTP redirect on handshake to new repo
338
339 $ cd $TESTTMP
340
341 $ hg init redirector
342 $ hg init redirected
343 $ cd redirected
344 $ touch foo
345 $ hg -q commit -A -m initial
346 $ cd ..
347
348 $ cat > paths.conf << EOF
349 > [paths]
350 > / = $TESTTMP/*
351 > EOF
352
353 $ cat > redirectext.py << EOF
354 > from mercurial import extensions, wireprotoserver
355 > def wrappedcallhttp(orig, repo, req, res, proto, cmd):
356 > path = req.advertisedurl[len(req.advertisedbaseurl):]
357 > if not path.startswith(b'/redirector'):
358 > return orig(repo, req, res, proto, cmd)
359 > relpath = path[len(b'/redirector'):]
360 > res.status = b'301 Redirect'
361 > newurl = b'%s/redirected%s' % (req.baseurl, relpath)
362 > if not repo.ui.configbool('testing', 'redirectqs', True) and b'?' in newurl:
363 > newurl = newurl[0:newurl.index(b'?')]
364 > res.headers[b'Location'] = newurl
365 > res.headers[b'Content-Type'] = b'text/plain'
366 > res.setbodybytes(b'redirected')
367 > return True
368 >
369 > extensions.wrapfunction(wireprotoserver, '_callhttp', wrappedcallhttp)
370 > EOF
371
372 $ hg --config extensions.redirect=$TESTTMP/redirectext.py \
373 > --config server.compressionengines=zlib \
374 > serve --web-conf paths.conf --pid-file hg.pid -p $HGPORT -d
375 $ cat hg.pid > $DAEMON_PIDS
376
377 Verify our HTTP 301 is served properly
378
379 $ hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT << EOF
380 > httprequest GET /redirector?cmd=capabilities
381 > user-agent: test
382 > EOF
383 using raw connection to peer
384 s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
385 s> Accept-Encoding: identity\r\n
386 s> user-agent: test\r\n
387 s> host: $LOCALIP:$HGPORT\r\n (glob)
388 s> \r\n
389 s> makefile('rb', None)
390 s> HTTP/1.1 301 Redirect\r\n
391 s> Server: testing stub value\r\n
392 s> Date: $HTTP_DATE$\r\n
393 s> Location: http://$LOCALIP:$HGPORT/redirected?cmd=capabilities\r\n (glob)
394 s> Content-Type: text/plain\r\n
395 s> Content-Length: 10\r\n
396 s> \r\n
397 s> redirected
398 s> GET /redirected?cmd=capabilities HTTP/1.1\r\n
399 s> Accept-Encoding: identity\r\n
400 s> user-agent: test\r\n
401 s> host: $LOCALIP:$HGPORT\r\n (glob)
402 s> \r\n
403 s> makefile('rb', None)
404 s> HTTP/1.1 200 Script output follows\r\n
405 s> Server: testing stub value\r\n
406 s> Date: $HTTP_DATE$\r\n
407 s> Content-Type: application/mercurial-0.1\r\n
408 s> Content-Length: 453\r\n
409 s> \r\n
410 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
411
412 Test with the HTTP peer
413
414 $ hg --verbose debugwireproto http://$LOCALIP:$HGPORT/redirector << EOF
415 > command heads
416 > EOF
417 s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
418 s> Accept-Encoding: identity\r\n
419 s> accept: application/mercurial-0.1\r\n
420 s> host: $LOCALIP:$HGPORT\r\n (glob)
421 s> user-agent: Mercurial debugwireproto\r\n
422 s> \r\n
423 s> makefile('rb', None)
424 s> HTTP/1.1 301 Redirect\r\n
425 s> Server: testing stub value\r\n
426 s> Date: $HTTP_DATE$\r\n
427 s> Location: http://$LOCALIP:$HGPORT/redirected?cmd=capabilities\r\n (glob)
428 s> Content-Type: text/plain\r\n
429 s> Content-Length: 10\r\n
430 s> \r\n
431 s> redirected
432 s> GET /redirected?cmd=capabilities HTTP/1.1\r\n
433 s> Accept-Encoding: identity\r\n
434 s> accept: application/mercurial-0.1\r\n
435 s> host: $LOCALIP:$HGPORT\r\n (glob)
436 s> user-agent: Mercurial debugwireproto\r\n
437 s> \r\n
438 s> makefile('rb', None)
439 s> HTTP/1.1 200 Script output follows\r\n
440 s> Server: testing stub value\r\n
441 s> Date: $HTTP_DATE$\r\n
442 s> Content-Type: application/mercurial-0.1\r\n
443 s> Content-Length: 453\r\n
444 s> \r\n
445 real URL is http://$LOCALIP:$HGPORT/redirected (glob)
446 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
447 sending heads command
448 s> GET /redirected?cmd=heads HTTP/1.1\r\n
449 s> Accept-Encoding: identity\r\n
450 s> vary: X-HgProto-1\r\n
451 s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
452 s> accept: application/mercurial-0.1\r\n
453 s> host: $LOCALIP:$HGPORT\r\n (glob)
454 s> user-agent: Mercurial debugwireproto\r\n
455 s> \r\n
456 s> makefile('rb', None)
457 s> HTTP/1.1 200 Script output follows\r\n
458 s> Server: testing stub value\r\n
459 s> Date: $HTTP_DATE$\r\n
460 s> Content-Type: application/mercurial-0.1\r\n
461 s> Content-Length: 41\r\n
462 s> \r\n
463 s> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n
464 response: [b'\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL']
465
466 $ killdaemons.py
467
468 Now test a variation where we strip the query string from the redirect URL.
469 (SCM Manager apparently did this and clients would recover from it)
470
471 $ hg --config extensions.redirect=$TESTTMP/redirectext.py \
472 > --config server.compressionengines=zlib \
473 > --config testing.redirectqs=false \
474 > serve --web-conf paths.conf --pid-file hg.pid -p $HGPORT -d
475 $ cat hg.pid > $DAEMON_PIDS
476
477 $ hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT << EOF
478 > httprequest GET /redirector?cmd=capabilities
479 > user-agent: test
480 > EOF
481 using raw connection to peer
482 s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
483 s> Accept-Encoding: identity\r\n
484 s> user-agent: test\r\n
485 s> host: $LOCALIP:$HGPORT\r\n (glob)
486 s> \r\n
487 s> makefile('rb', None)
488 s> HTTP/1.1 301 Redirect\r\n
489 s> Server: testing stub value\r\n
490 s> Date: $HTTP_DATE$\r\n
491 s> Location: http://$LOCALIP:$HGPORT/redirected\r\n (glob)
492 s> Content-Type: text/plain\r\n
493 s> Content-Length: 10\r\n
494 s> \r\n
495 s> redirected
496 s> GET /redirected HTTP/1.1\r\n
497 s> Accept-Encoding: identity\r\n
498 s> user-agent: test\r\n
499 s> host: $LOCALIP:$HGPORT\r\n (glob)
500 s> \r\n
501 s> makefile('rb', None)
502 s> HTTP/1.1 200 Script output follows\r\n
503 s> Server: testing stub value\r\n
504 s> Date: $HTTP_DATE$\r\n
505 s> ETag: W/"*"\r\n (glob)
506 s> Content-Type: text/html; charset=ascii\r\n
507 s> Transfer-Encoding: chunked\r\n
508 s> \r\n
509 s> 414\r\n
510 s> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n
511 s> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">\n
512 s> <head>\n
513 s> <link rel="icon" href="/redirected/static/hgicon.png" type="image/png" />\n
514 s> <meta name="robots" content="index, nofollow" />\n
515 s> <link rel="stylesheet" href="/redirected/static/style-paper.css" type="text/css" />\n
516 s> <script type="text/javascript" src="/redirected/static/mercurial.js"></script>\n
517 s> \n
518 s> <title>redirected: log</title>\n
519 s> <link rel="alternate" type="application/atom+xml"\n
520 s> href="/redirected/atom-log" title="Atom feed for redirected" />\n
521 s> <link rel="alternate" type="application/rss+xml"\n
522 s> href="/redirected/rss-log" title="RSS feed for redirected" />\n
523 s> </head>\n
524 s> <body>\n
525 s> \n
526 s> <div class="container">\n
527 s> <div class="menu">\n
528 s> <div class="logo">\n
529 s> <a href="https://mercurial-scm.org/">\n
530 s> <img src="/redirected/static/hglogo.png" alt="mercurial" /></a>\n
531 s> </div>\n
532 s> <ul>\n
533 s> <li class="active">log</li>\n
534 s> <li><a href="/redirected/graph/tip">graph</a></li>\n
535 s> <li><a href="/redirected/tags">tags</a></li>\n
536 s> <li><a href="
537 s> \r\n
538 s> 810\r\n
539 s> /redirected/bookmarks">bookmarks</a></li>\n
540 s> <li><a href="/redirected/branches">branches</a></li>\n
541 s> </ul>\n
542 s> <ul>\n
543 s> <li><a href="/redirected/rev/tip">changeset</a></li>\n
544 s> <li><a href="/redirected/file/tip">browse</a></li>\n
545 s> </ul>\n
546 s> <ul>\n
547 s> \n
548 s> </ul>\n
549 s> <ul>\n
550 s> <li><a href="/redirected/help">help</a></li>\n
551 s> </ul>\n
552 s> <div class="atom-logo">\n
553 s> <a href="/redirected/atom-log" title="subscribe to atom feed">\n
554 s> <img class="atom-logo" src="/redirected/static/feed-icon-14x14.png" alt="atom feed" />\n
555 s> </a>\n
556 s> </div>\n
557 s> </div>\n
558 s> \n
559 s> <div class="main">\n
560 s> <h2 class="breadcrumb"><a href="/">Mercurial</a> &gt; <a href="/redirected">redirected</a> </h2>\n
561 s> <h3>log</h3>\n
562 s> \n
563 s> \n
564 s> <form class="search" action="/redirected/log">\n
565 s> \n
566 s> <p><input name="rev" id="search1" type="text" size="30" value="" /></p>\n
567 s> <div id="hint">Find changesets by keywords (author, files, the commit message), revision\n
568 s> number or hash, or <a href="/redirected/help/revsets">revset expression</a>.</div>\n
569 s> </form>\n
570 s> \n
571 s> <div class="navigate">\n
572 s> <a href="/redirected/shortlog/tip?revcount=30">less</a>\n
573 s> <a href="/redirected/shortlog/tip?revcount=120">more</a>\n
574 s> | rev 0: <a href="/redirected/shortlog/96ee1d7354c4">(0)</a> <a href="/redirected/shortlog/tip">tip</a> \n
575 s> </div>\n
576 s> \n
577 s> <table class="bigtable">\n
578 s> <thead>\n
579 s> <tr>\n
580 s> <th class="age">age</th>\n
581 s> <th class="author">author</th>\n
582 s> <th class="description">description</th>\n
583 s> </tr>\n
584 s> </thead>\n
585 s> <tbody class="stripes2">\n
586 s> <tr>\n
587 s> <td class="age">Thu, 01 Jan 1970 00:00:00 +0000</td>\n
588 s> <td class="author">test</td>\n
589 s> <td class="description">\n
590 s> <a href="/redirected/rev/96ee1d7354c4">initial</a>\n
591 s> <span class="phase">draft</span> <span class="branchhead">default</span> <span class="tag">tip</span> \n
592 s> </td>\n
593 s> </tr>\n
594 s> \n
595 s> </tbody>\n
596 s> </table>\n
597 s> \n
598 s> <div class="navigate">\n
599 s> <a href="/redirected/shortlog/tip?revcount=30">less</a>\n
600 s> <a href="/redirected/shortlog/tip?revcount=120">more</a>\n
601 s> | rev 0: <a href="/redirected/shortlog/96ee1d7354c4">(0)</a> <a href="/redirected/shortlog/tip">tip</a> \n
602 s> </div>\n
603 s> \n
604 s> <script type="text/javascript">\n
605 s> ajaxScrollInit(\n
606 s> \'/redirected/shortlog/%next%\',\n
607 s> \'\', <!-- NEXTHASH\n
608 s> function (htmlText) {
609 s> \r\n
610 s> 14a\r\n
611 s> \n
612 s> var m = htmlText.match(/\'(\\w+)\', <!-- NEXTHASH/);\n
613 s> return m ? m[1] : null;\n
614 s> },\n
615 s> \'.bigtable > tbody\',\n
616 s> \'<tr class="%class%">\\\n
617 s> <td colspan="3" style="text-align: center;">%text%</td>\\\n
618 s> </tr>\'\n
619 s> );\n
620 s> </script>\n
621 s> \n
622 s> </div>\n
623 s> </div>\n
624 s> \n
625 s> \n
626 s> \n
627 s> </body>\n
628 s> </html>\n
629 s> \n
630 s> \r\n
631 s> 0\r\n
632 s> \r\n
633
634 $ hg --verbose debugwireproto http://$LOCALIP:$HGPORT/redirector << EOF
635 > command heads
636 > EOF
637 s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
638 s> Accept-Encoding: identity\r\n
639 s> accept: application/mercurial-0.1\r\n
640 s> host: $LOCALIP:$HGPORT\r\n (glob)
641 s> user-agent: Mercurial debugwireproto\r\n
642 s> \r\n
643 s> makefile('rb', None)
644 s> HTTP/1.1 301 Redirect\r\n
645 s> Server: testing stub value\r\n
646 s> Date: $HTTP_DATE$\r\n
647 s> Location: http://$LOCALIP:$HGPORT/redirected\r\n (glob)
648 s> Content-Type: text/plain\r\n
649 s> Content-Length: 10\r\n
650 s> \r\n
651 s> redirected
652 s> GET /redirected HTTP/1.1\r\n
653 s> Accept-Encoding: identity\r\n
654 s> accept: application/mercurial-0.1\r\n
655 s> host: $LOCALIP:$HGPORT\r\n (glob)
656 s> user-agent: Mercurial debugwireproto\r\n
657 s> \r\n
658 s> makefile('rb', None)
659 s> HTTP/1.1 200 Script output follows\r\n
660 s> Server: testing stub value\r\n
661 s> Date: $HTTP_DATE$\r\n
662 s> ETag: W/"*"\r\n (glob)
663 s> Content-Type: text/html; charset=ascii\r\n
664 s> Transfer-Encoding: chunked\r\n
665 s> \r\n
666 real URL is http://$LOCALIP:$HGPORT/redirected (glob)
667 s> 414\r\n
668 s> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n
669 s> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">\n
670 s> <head>\n
671 s> <link rel="icon" href="/redirected/static/hgicon.png" type="image/png" />\n
672 s> <meta name="robots" content="index, nofollow" />\n
673 s> <link rel="stylesheet" href="/redirected/static/style-paper.css" type="text/css" />\n
674 s> <script type="text/javascript" src="/redirected/static/mercurial.js"></script>\n
675 s> \n
676 s> <title>redirected: log</title>\n
677 s> <link rel="alternate" type="application/atom+xml"\n
678 s> href="/redirected/atom-log" title="Atom feed for redirected" />\n
679 s> <link rel="alternate" type="application/rss+xml"\n
680 s> href="/redirected/rss-log" title="RSS feed for redirected" />\n
681 s> </head>\n
682 s> <body>\n
683 s> \n
684 s> <div class="container">\n
685 s> <div class="menu">\n
686 s> <div class="logo">\n
687 s> <a href="https://mercurial-scm.org/">\n
688 s> <img src="/redirected/static/hglogo.png" alt="mercurial" /></a>\n
689 s> </div>\n
690 s> <ul>\n
691 s> <li class="active">log</li>\n
692 s> <li><a href="/redirected/graph/tip">graph</a></li>\n
693 s> <li><a href="/redirected/tags">tags</a
694 s> GET /redirected?cmd=capabilities HTTP/1.1\r\n
695 s> Accept-Encoding: identity\r\n
696 s> accept: application/mercurial-0.1\r\n
697 s> host: $LOCALIP:$HGPORT\r\n (glob)
698 s> user-agent: Mercurial debugwireproto\r\n
699 s> \r\n
700 s> makefile('rb', None)
701 s> HTTP/1.1 200 Script output follows\r\n
702 s> Server: testing stub value\r\n
703 s> Date: $HTTP_DATE$\r\n
704 s> Content-Type: application/mercurial-0.1\r\n
705 s> Content-Length: 453\r\n
706 s> \r\n
707 real URL is http://$LOCALIP:$HGPORT/redirected (glob)
708 s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
709 sending heads command
710 s> GET /redirected?cmd=heads HTTP/1.1\r\n
711 s> Accept-Encoding: identity\r\n
712 s> vary: X-HgProto-1\r\n
713 s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
714 s> accept: application/mercurial-0.1\r\n
715 s> host: $LOCALIP:$HGPORT\r\n (glob)
716 s> user-agent: Mercurial debugwireproto\r\n
717 s> \r\n
718 s> makefile('rb', None)
719 s> HTTP/1.1 200 Script output follows\r\n
720 s> Server: testing stub value\r\n
721 s> Date: $HTTP_DATE$\r\n
722 s> Content-Type: application/mercurial-0.1\r\n
723 s> Content-Length: 41\r\n
724 s> \r\n
725 s> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n
726 response: [b'\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL']
General Comments 0
You need to be logged in to leave comments. Login now