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