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> \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>
ageauthordescription
Thu, 01 Jan 1970 00:00:00 +0000test\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> \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>