diff --git a/mercurial/httppeer.py b/mercurial/httppeer.py
--- a/mercurial/httppeer.py
+++ b/mercurial/httppeer.py
@@ -328,13 +328,24 @@ def sendrequest(ui, opener, req):
return res
+class RedirectedRepoError(error.RepoError):
+ def __init__(self, msg, respurl):
+ super(RedirectedRepoError, self).__init__(msg)
+ self.respurl = respurl
+
def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
allowcbor=False):
# record the url we got redirected to
+ redirected = False
respurl = pycompat.bytesurl(resp.geturl())
if respurl.endswith(qs):
respurl = respurl[:-len(qs)]
+ qsdropped = False
+ else:
+ qsdropped = True
+
if baseurl.rstrip('/') != respurl.rstrip('/'):
+ redirected = True
if not ui.quiet:
ui.warn(_('real URL is %s\n') % respurl)
@@ -351,10 +362,16 @@ def parsev1commandresponse(ui, baseurl,
# application/hg-changegroup. We don't support such old servers.
if not proto.startswith('application/mercurial-'):
ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
- raise error.RepoError(
- _("'%s' does not appear to be an hg repository:\n"
- "---%%<--- (%s)\n%s\n---%%<---\n")
- % (safeurl, proto or 'no content-type', resp.read(1024)))
+ msg = _("'%s' does not appear to be an hg repository:\n"
+ "---%%<--- (%s)\n%s\n---%%<---\n") % (
+ safeurl, proto or 'no content-type', resp.read(1024))
+
+ # Some servers may strip the query string from the redirect. We
+ # raise a special error type so callers can react to this specially.
+ if redirected and qsdropped:
+ raise RedirectedRepoError(msg, respurl)
+ else:
+ raise error.RepoError(msg)
try:
subtype = proto.split('-', 1)[1]
@@ -434,8 +451,6 @@ class httppeer(wireprotov1peer.wirepeer)
# End of ipeercommands interface.
- # look up capabilities only when needed
-
def _callstream(self, cmd, _compressible=False, **args):
args = pycompat.byteskwargs(args)
@@ -853,12 +868,32 @@ def performhandshake(ui, url, opener, re
req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
capable, url, 'capabilities',
args)
-
resp = sendrequest(ui, opener, req)
- respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
- compressible=False,
- allowcbor=advertisev2)
+ # The server may redirect us to the repo root, stripping the
+ # ?cmd=capabilities query string from the URL. The server would likely
+ # return HTML in this case and ``parsev1commandresponse()`` would raise.
+ # We catch this special case and re-issue the capabilities request against
+ # the new URL.
+ #
+ # We should ideally not do this, as a redirect that drops the query
+ # string from the URL is arguably a server bug. (Garbage in, garbage out).
+ # However, Mercurial clients for several years appeared to handle this
+ # issue without behavior degradation. And according to issue 5860, it may
+ # be a longstanding bug in some server implementations. So we allow a
+ # redirect that drops the query string to "just work."
+ try:
+ respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
+ compressible=False,
+ allowcbor=advertisev2)
+ except RedirectedRepoError as e:
+ req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
+ capable, e.respurl,
+ 'capabilities', args)
+ resp = sendrequest(ui, opener, req)
+ respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
+ compressible=False,
+ allowcbor=advertisev2)
try:
rawdata = resp.read()
diff --git a/tests/test-http-protocol.t b/tests/test-http-protocol.t
--- a/tests/test-http-protocol.t
+++ b/tests/test-http-protocol.t
@@ -333,3 +333,394 @@ Client with HTTPv2 enabled automatically
response: [b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']
$ killdaemons.py
+
+HTTP client follows HTTP redirect on handshake to new repo
+
+ $ cd $TESTTMP
+
+ $ hg init redirector
+ $ hg init redirected
+ $ cd redirected
+ $ touch foo
+ $ hg -q commit -A -m initial
+ $ cd ..
+
+ $ cat > paths.conf << EOF
+ > [paths]
+ > / = $TESTTMP/*
+ > EOF
+
+ $ cat > redirectext.py << EOF
+ > from mercurial import extensions, wireprotoserver
+ > def wrappedcallhttp(orig, repo, req, res, proto, cmd):
+ > path = req.advertisedurl[len(req.advertisedbaseurl):]
+ > if not path.startswith(b'/redirector'):
+ > return orig(repo, req, res, proto, cmd)
+ > relpath = path[len(b'/redirector'):]
+ > res.status = b'301 Redirect'
+ > newurl = b'%s/redirected%s' % (req.baseurl, relpath)
+ > if not repo.ui.configbool('testing', 'redirectqs', True) and b'?' in newurl:
+ > newurl = newurl[0:newurl.index(b'?')]
+ > res.headers[b'Location'] = newurl
+ > res.headers[b'Content-Type'] = b'text/plain'
+ > res.setbodybytes(b'redirected')
+ > return True
+ >
+ > extensions.wrapfunction(wireprotoserver, '_callhttp', wrappedcallhttp)
+ > EOF
+
+ $ hg --config extensions.redirect=$TESTTMP/redirectext.py \
+ > --config server.compressionengines=zlib \
+ > serve --web-conf paths.conf --pid-file hg.pid -p $HGPORT -d
+ $ cat hg.pid > $DAEMON_PIDS
+
+Verify our HTTP 301 is served properly
+
+ $ hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT << EOF
+ > httprequest GET /redirector?cmd=capabilities
+ > user-agent: test
+ > EOF
+ using raw connection to peer
+ s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
+ s> Accept-Encoding: identity\r\n
+ s> user-agent: test\r\n
+ s> host: $LOCALIP:$HGPORT\r\n (glob)
+ s> \r\n
+ s> makefile('rb', None)
+ s> HTTP/1.1 301 Redirect\r\n
+ s> Server: testing stub value\r\n
+ s> Date: $HTTP_DATE$\r\n
+ s> Location: http://$LOCALIP:$HGPORT/redirected?cmd=capabilities\r\n (glob)
+ s> Content-Type: text/plain\r\n
+ s> Content-Length: 10\r\n
+ s> \r\n
+ s> redirected
+ s> GET /redirected?cmd=capabilities HTTP/1.1\r\n
+ s> Accept-Encoding: identity\r\n
+ s> user-agent: test\r\n
+ s> host: $LOCALIP:$HGPORT\r\n (glob)
+ s> \r\n
+ s> makefile('rb', None)
+ s> HTTP/1.1 200 Script output follows\r\n
+ s> Server: testing stub value\r\n
+ s> Date: $HTTP_DATE$\r\n
+ s> Content-Type: application/mercurial-0.1\r\n
+ s> Content-Length: 453\r\n
+ s> \r\n
+ 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
+
+Test with the HTTP peer
+
+ $ hg --verbose debugwireproto http://$LOCALIP:$HGPORT/redirector << EOF
+ > command heads
+ > EOF
+ s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
+ s> Accept-Encoding: identity\r\n
+ s> accept: application/mercurial-0.1\r\n
+ s> host: $LOCALIP:$HGPORT\r\n (glob)
+ s> user-agent: Mercurial debugwireproto\r\n
+ s> \r\n
+ s> makefile('rb', None)
+ s> HTTP/1.1 301 Redirect\r\n
+ s> Server: testing stub value\r\n
+ s> Date: $HTTP_DATE$\r\n
+ s> Location: http://$LOCALIP:$HGPORT/redirected?cmd=capabilities\r\n (glob)
+ s> Content-Type: text/plain\r\n
+ s> Content-Length: 10\r\n
+ s> \r\n
+ s> redirected
+ s> GET /redirected?cmd=capabilities HTTP/1.1\r\n
+ s> Accept-Encoding: identity\r\n
+ s> accept: application/mercurial-0.1\r\n
+ s> host: $LOCALIP:$HGPORT\r\n (glob)
+ s> user-agent: Mercurial debugwireproto\r\n
+ s> \r\n
+ s> makefile('rb', None)
+ s> HTTP/1.1 200 Script output follows\r\n
+ s> Server: testing stub value\r\n
+ s> Date: $HTTP_DATE$\r\n
+ s> Content-Type: application/mercurial-0.1\r\n
+ s> Content-Length: 453\r\n
+ s> \r\n
+ real URL is http://$LOCALIP:$HGPORT/redirected (glob)
+ 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
+ sending heads command
+ s> GET /redirected?cmd=heads HTTP/1.1\r\n
+ s> Accept-Encoding: identity\r\n
+ s> vary: X-HgProto-1\r\n
+ s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
+ s> accept: application/mercurial-0.1\r\n
+ s> host: $LOCALIP:$HGPORT\r\n (glob)
+ s> user-agent: Mercurial debugwireproto\r\n
+ s> \r\n
+ s> makefile('rb', None)
+ s> HTTP/1.1 200 Script output follows\r\n
+ s> Server: testing stub value\r\n
+ s> Date: $HTTP_DATE$\r\n
+ s> Content-Type: application/mercurial-0.1\r\n
+ s> Content-Length: 41\r\n
+ s> \r\n
+ s> 96ee1d7354c4ad7372047672c36a1f561e3a6a4c\n
+ response: [b'\x96\xee\x1dsT\xc4\xadsr\x04vr\xc3j\x1fV\x1e:jL']
+
+ $ killdaemons.py
+
+Now test a variation where we strip the query string from the redirect URL.
+(SCM Manager apparently did this and clients would recover from it)
+
+ $ hg --config extensions.redirect=$TESTTMP/redirectext.py \
+ > --config server.compressionengines=zlib \
+ > --config testing.redirectqs=false \
+ > serve --web-conf paths.conf --pid-file hg.pid -p $HGPORT -d
+ $ cat hg.pid > $DAEMON_PIDS
+
+ $ hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT << EOF
+ > httprequest GET /redirector?cmd=capabilities
+ > user-agent: test
+ > EOF
+ using raw connection to peer
+ s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
+ s> Accept-Encoding: identity\r\n
+ s> user-agent: test\r\n
+ s> host: $LOCALIP:$HGPORT\r\n (glob)
+ s> \r\n
+ s> makefile('rb', None)
+ s> HTTP/1.1 301 Redirect\r\n
+ s> Server: testing stub value\r\n
+ s> Date: $HTTP_DATE$\r\n
+ s> Location: http://$LOCALIP:$HGPORT/redirected\r\n (glob)
+ s> Content-Type: text/plain\r\n
+ s> Content-Length: 10\r\n
+ s> \r\n
+ s> redirected
+ s> GET /redirected HTTP/1.1\r\n
+ s> Accept-Encoding: identity\r\n
+ s> user-agent: test\r\n
+ s> host: $LOCALIP:$HGPORT\r\n (glob)
+ s> \r\n
+ s> makefile('rb', None)
+ s> HTTP/1.1 200 Script output follows\r\n
+ s> Server: testing stub value\r\n
+ s> Date: $HTTP_DATE$\r\n
+ s> ETag: W/"*"\r\n (glob)
+ s> Content-Type: text/html; charset=ascii\r\n
+ s> Transfer-Encoding: chunked\r\n
+ s> \r\n
+ s> 414\r\n
+ s> \n
+ s> \n
+ s>
\n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> redirected: log\n
+ s> href="/redirected/atom-log" title="Atom feed for redirected" />\n
+ s> href="/redirected/rss-log" title="RSS feed for redirected" />\n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s>
\n
+ s>
\n
+ s>
log
\n
+ s> \n
+ s> \n
+ s>
\n
+ s> \n
+ s>
\n
+ s> \n
+ s>
\n
+ s> \n
+ s> \n
+ s> age | \n
+ s> author | \n
+ s> description | \n
+ s>
\n
+ s> \n
+ s> \n
+ s> \n
+ s> Thu, 01 Jan 1970 00:00:00 +0000 | \n
+ s> test | \n
+ s> \n
+ s> initial\n
+ s> draft default tip \n
+ s> | \n
+ s>
\n
+ s> \n
+ s> \n
+ s>
\n
+ s> \n
+ s>
\n
+ s> \n
+ s> \n
+ s> \n
+ s>
\n
+ s>
\n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> \r\n
+ s> 0\r\n
+ s> \r\n
+
+ $ hg --verbose debugwireproto http://$LOCALIP:$HGPORT/redirector << EOF
+ > command heads
+ > EOF
+ s> GET /redirector?cmd=capabilities HTTP/1.1\r\n
+ s> Accept-Encoding: identity\r\n
+ s> accept: application/mercurial-0.1\r\n
+ s> host: $LOCALIP:$HGPORT\r\n (glob)
+ s> user-agent: Mercurial debugwireproto\r\n
+ s> \r\n
+ s> makefile('rb', None)
+ s> HTTP/1.1 301 Redirect\r\n
+ s> Server: testing stub value\r\n
+ s> Date: $HTTP_DATE$\r\n
+ s> Location: http://$LOCALIP:$HGPORT/redirected\r\n (glob)
+ s> Content-Type: text/plain\r\n
+ s> Content-Length: 10\r\n
+ s> \r\n
+ s> redirected
+ s> GET /redirected HTTP/1.1\r\n
+ s> Accept-Encoding: identity\r\n
+ s> accept: application/mercurial-0.1\r\n
+ s> host: $LOCALIP:$HGPORT\r\n (glob)
+ s> user-agent: Mercurial debugwireproto\r\n
+ s> \r\n
+ s> makefile('rb', None)
+ s> HTTP/1.1 200 Script output follows\r\n
+ s> Server: testing stub value\r\n
+ s> Date: $HTTP_DATE$\r\n
+ s> ETag: W/"*"\r\n (glob)
+ s> Content-Type: text/html; charset=ascii\r\n
+ s> Transfer-Encoding: chunked\r\n
+ s> \r\n
+ real URL is http://$LOCALIP:$HGPORT/redirected (glob)
+ s> 414\r\n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s> redirected: log\n
+ s> href="/redirected/atom-log" title="Atom feed for redirected" />\n
+ s> href="/redirected/rss-log" title="RSS feed for redirected" />\n
+ s> \n
+ s> \n
+ s> \n
+ s> \n
+ s>