90 def length(self): |
90 def length(self): |
91 return sum(f.length for f in self._fileobjs) |
91 return sum(f.length for f in self._fileobjs) |
92 |
92 |
93 def read(self, amt=None): |
93 def read(self, amt=None): |
94 if amt <= 0: |
94 if amt <= 0: |
95 return ''.join(f.read() for f in self._fileobjs) |
95 return b''.join(f.read() for f in self._fileobjs) |
96 parts = [] |
96 parts = [] |
97 while amt and self._index < len(self._fileobjs): |
97 while amt and self._index < len(self._fileobjs): |
98 parts.append(self._fileobjs[self._index].read(amt)) |
98 parts.append(self._fileobjs[self._index].read(amt)) |
99 got = len(parts[-1]) |
99 got = len(parts[-1]) |
100 if got < amt: |
100 if got < amt: |
101 self._index += 1 |
101 self._index += 1 |
102 amt -= got |
102 amt -= got |
103 return ''.join(parts) |
103 return b''.join(parts) |
104 |
104 |
105 def seek(self, offset, whence=os.SEEK_SET): |
105 def seek(self, offset, whence=os.SEEK_SET): |
106 if whence != os.SEEK_SET: |
106 if whence != os.SEEK_SET: |
107 raise NotImplementedError( |
107 raise NotImplementedError( |
108 '_multifile does not support anything other' |
108 b'_multifile does not support anything other' |
109 ' than os.SEEK_SET for whence on seek()' |
109 b' than os.SEEK_SET for whence on seek()' |
110 ) |
110 ) |
111 if offset != 0: |
111 if offset != 0: |
112 raise NotImplementedError( |
112 raise NotImplementedError( |
113 '_multifile only supports seeking to start, but that ' |
113 b'_multifile only supports seeking to start, but that ' |
114 'could be fixed if you need it' |
114 b'could be fixed if you need it' |
115 ) |
115 ) |
116 for f in self._fileobjs: |
116 for f in self._fileobjs: |
117 f.seek(0) |
117 f.seek(0) |
118 self._index = 0 |
118 self._index = 0 |
119 |
119 |
129 ``capablefn`` is a function to evaluate a capability. |
129 ``capablefn`` is a function to evaluate a capability. |
130 |
130 |
131 ``cmd``, ``args``, and ``data`` define the command, its arguments, and |
131 ``cmd``, ``args``, and ``data`` define the command, its arguments, and |
132 raw data to pass to it. |
132 raw data to pass to it. |
133 """ |
133 """ |
134 if cmd == 'pushkey': |
134 if cmd == b'pushkey': |
135 args['data'] = '' |
135 args[b'data'] = b'' |
136 data = args.pop('data', None) |
136 data = args.pop(b'data', None) |
137 headers = args.pop('headers', {}) |
137 headers = args.pop(b'headers', {}) |
138 |
138 |
139 ui.debug("sending %s command\n" % cmd) |
139 ui.debug(b"sending %s command\n" % cmd) |
140 q = [('cmd', cmd)] |
140 q = [(b'cmd', cmd)] |
141 headersize = 0 |
141 headersize = 0 |
142 # Important: don't use self.capable() here or else you end up |
142 # Important: don't use self.capable() here or else you end up |
143 # with infinite recursion when trying to look up capabilities |
143 # with infinite recursion when trying to look up capabilities |
144 # for the first time. |
144 # for the first time. |
145 postargsok = caps is not None and 'httppostargs' in caps |
145 postargsok = caps is not None and b'httppostargs' in caps |
146 |
146 |
147 # Send arguments via POST. |
147 # Send arguments via POST. |
148 if postargsok and args: |
148 if postargsok and args: |
149 strargs = urlreq.urlencode(sorted(args.items())) |
149 strargs = urlreq.urlencode(sorted(args.items())) |
150 if not data: |
150 if not data: |
160 headers[r'X-HgArgs-Post'] = len(strargs) |
160 headers[r'X-HgArgs-Post'] = len(strargs) |
161 elif args: |
161 elif args: |
162 # Calling self.capable() can infinite loop if we are calling |
162 # Calling self.capable() can infinite loop if we are calling |
163 # "capabilities". But that command should never accept wire |
163 # "capabilities". But that command should never accept wire |
164 # protocol arguments. So this should never happen. |
164 # protocol arguments. So this should never happen. |
165 assert cmd != 'capabilities' |
165 assert cmd != b'capabilities' |
166 httpheader = capablefn('httpheader') |
166 httpheader = capablefn(b'httpheader') |
167 if httpheader: |
167 if httpheader: |
168 headersize = int(httpheader.split(',', 1)[0]) |
168 headersize = int(httpheader.split(b',', 1)[0]) |
169 |
169 |
170 # Send arguments via HTTP headers. |
170 # Send arguments via HTTP headers. |
171 if headersize > 0: |
171 if headersize > 0: |
172 # The headers can typically carry more data than the URL. |
172 # The headers can typically carry more data than the URL. |
173 encargs = urlreq.urlencode(sorted(args.items())) |
173 encargs = urlreq.urlencode(sorted(args.items())) |
174 for header, value in encodevalueinheaders( |
174 for header, value in encodevalueinheaders( |
175 encargs, 'X-HgArg', headersize |
175 encargs, b'X-HgArg', headersize |
176 ): |
176 ): |
177 headers[header] = value |
177 headers[header] = value |
178 # Send arguments via query string (Mercurial <1.9). |
178 # Send arguments via query string (Mercurial <1.9). |
179 else: |
179 else: |
180 q += sorted(args.items()) |
180 q += sorted(args.items()) |
181 |
181 |
182 qs = '?%s' % urlreq.urlencode(q) |
182 qs = b'?%s' % urlreq.urlencode(q) |
183 cu = "%s%s" % (repobaseurl, qs) |
183 cu = b"%s%s" % (repobaseurl, qs) |
184 size = 0 |
184 size = 0 |
185 if util.safehasattr(data, 'length'): |
185 if util.safehasattr(data, b'length'): |
186 size = data.length |
186 size = data.length |
187 elif data is not None: |
187 elif data is not None: |
188 size = len(data) |
188 size = len(data) |
189 if data is not None and r'Content-Type' not in headers: |
189 if data is not None and r'Content-Type' not in headers: |
190 headers[r'Content-Type'] = r'application/mercurial-0.1' |
190 headers[r'Content-Type'] = r'application/mercurial-0.1' |
196 # protocol parameters should only occur after the handshake. |
196 # protocol parameters should only occur after the handshake. |
197 protoparams = set() |
197 protoparams = set() |
198 |
198 |
199 mediatypes = set() |
199 mediatypes = set() |
200 if caps is not None: |
200 if caps is not None: |
201 mt = capablefn('httpmediatype') |
201 mt = capablefn(b'httpmediatype') |
202 if mt: |
202 if mt: |
203 protoparams.add('0.1') |
203 protoparams.add(b'0.1') |
204 mediatypes = set(mt.split(',')) |
204 mediatypes = set(mt.split(b',')) |
205 |
205 |
206 protoparams.add('partial-pull') |
206 protoparams.add(b'partial-pull') |
207 |
207 |
208 if '0.2tx' in mediatypes: |
208 if b'0.2tx' in mediatypes: |
209 protoparams.add('0.2') |
209 protoparams.add(b'0.2') |
210 |
210 |
211 if '0.2tx' in mediatypes and capablefn('compression'): |
211 if b'0.2tx' in mediatypes and capablefn(b'compression'): |
212 # We /could/ compare supported compression formats and prune |
212 # We /could/ compare supported compression formats and prune |
213 # non-mutually supported or error if nothing is mutually supported. |
213 # non-mutually supported or error if nothing is mutually supported. |
214 # For now, send the full list to the server and have it error. |
214 # For now, send the full list to the server and have it error. |
215 comps = [ |
215 comps = [ |
216 e.wireprotosupport().name |
216 e.wireprotosupport().name |
217 for e in util.compengines.supportedwireengines(util.CLIENTROLE) |
217 for e in util.compengines.supportedwireengines(util.CLIENTROLE) |
218 ] |
218 ] |
219 protoparams.add('comp=%s' % ','.join(comps)) |
219 protoparams.add(b'comp=%s' % b','.join(comps)) |
220 |
220 |
221 if protoparams: |
221 if protoparams: |
222 protoheaders = encodevalueinheaders( |
222 protoheaders = encodevalueinheaders( |
223 ' '.join(sorted(protoparams)), 'X-HgProto', headersize or 1024 |
223 b' '.join(sorted(protoparams)), b'X-HgProto', headersize or 1024 |
224 ) |
224 ) |
225 for header, value in protoheaders: |
225 for header, value in protoheaders: |
226 headers[header] = value |
226 headers[header] = value |
227 |
227 |
228 varyheaders = [] |
228 varyheaders = [] |
255 """Send a prepared HTTP request. |
255 """Send a prepared HTTP request. |
256 |
256 |
257 Returns the response object. |
257 Returns the response object. |
258 """ |
258 """ |
259 dbg = ui.debug |
259 dbg = ui.debug |
260 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'): |
260 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'): |
261 line = 'devel-peer-request: %s\n' |
261 line = b'devel-peer-request: %s\n' |
262 dbg( |
262 dbg( |
263 line |
263 line |
264 % '%s %s' |
264 % b'%s %s' |
265 % ( |
265 % ( |
266 pycompat.bytesurl(req.get_method()), |
266 pycompat.bytesurl(req.get_method()), |
267 pycompat.bytesurl(req.get_full_url()), |
267 pycompat.bytesurl(req.get_full_url()), |
268 ) |
268 ) |
269 ) |
269 ) |
270 hgargssize = None |
270 hgargssize = None |
271 |
271 |
272 for header, value in sorted(req.header_items()): |
272 for header, value in sorted(req.header_items()): |
273 header = pycompat.bytesurl(header) |
273 header = pycompat.bytesurl(header) |
274 value = pycompat.bytesurl(value) |
274 value = pycompat.bytesurl(value) |
275 if header.startswith('X-hgarg-'): |
275 if header.startswith(b'X-hgarg-'): |
276 if hgargssize is None: |
276 if hgargssize is None: |
277 hgargssize = 0 |
277 hgargssize = 0 |
278 hgargssize += len(value) |
278 hgargssize += len(value) |
279 else: |
279 else: |
280 dbg(line % ' %s %s' % (header, value)) |
280 dbg(line % b' %s %s' % (header, value)) |
281 |
281 |
282 if hgargssize is not None: |
282 if hgargssize is not None: |
283 dbg( |
283 dbg( |
284 line |
284 line |
285 % ' %d bytes of commands arguments in headers' |
285 % b' %d bytes of commands arguments in headers' |
286 % hgargssize |
286 % hgargssize |
287 ) |
287 ) |
288 data = _reqdata(req) |
288 data = _reqdata(req) |
289 if data is not None: |
289 if data is not None: |
290 length = getattr(data, 'length', None) |
290 length = getattr(data, 'length', None) |
291 if length is None: |
291 if length is None: |
292 length = len(data) |
292 length = len(data) |
293 dbg(line % ' %d bytes of data' % length) |
293 dbg(line % b' %d bytes of data' % length) |
294 |
294 |
295 start = util.timer() |
295 start = util.timer() |
296 |
296 |
297 res = None |
297 res = None |
298 try: |
298 try: |
299 res = opener.open(req) |
299 res = opener.open(req) |
300 except urlerr.httperror as inst: |
300 except urlerr.httperror as inst: |
301 if inst.code == 401: |
301 if inst.code == 401: |
302 raise error.Abort(_('authorization failed')) |
302 raise error.Abort(_(b'authorization failed')) |
303 raise |
303 raise |
304 except httplib.HTTPException as inst: |
304 except httplib.HTTPException as inst: |
305 ui.debug( |
305 ui.debug( |
306 'http error requesting %s\n' % util.hidepassword(req.get_full_url()) |
306 b'http error requesting %s\n' |
|
307 % util.hidepassword(req.get_full_url()) |
307 ) |
308 ) |
308 ui.traceback() |
309 ui.traceback() |
309 raise IOError(None, inst) |
310 raise IOError(None, inst) |
310 finally: |
311 finally: |
311 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'): |
312 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'): |
312 code = res.code if res else -1 |
313 code = res.code if res else -1 |
313 dbg( |
314 dbg( |
314 line |
315 line |
315 % ' finished in %.4f seconds (%d)' |
316 % b' finished in %.4f seconds (%d)' |
316 % (util.timer() - start, code) |
317 % (util.timer() - start, code) |
317 ) |
318 ) |
318 |
319 |
319 # Insert error handlers for common I/O failures. |
320 # Insert error handlers for common I/O failures. |
320 urlmod.wrapresponse(res) |
321 urlmod.wrapresponse(res) |
338 respurl = respurl[: -len(qs)] |
339 respurl = respurl[: -len(qs)] |
339 qsdropped = False |
340 qsdropped = False |
340 else: |
341 else: |
341 qsdropped = True |
342 qsdropped = True |
342 |
343 |
343 if baseurl.rstrip('/') != respurl.rstrip('/'): |
344 if baseurl.rstrip(b'/') != respurl.rstrip(b'/'): |
344 redirected = True |
345 redirected = True |
345 if not ui.quiet: |
346 if not ui.quiet: |
346 ui.warn(_('real URL is %s\n') % respurl) |
347 ui.warn(_(b'real URL is %s\n') % respurl) |
347 |
348 |
348 try: |
349 try: |
349 proto = pycompat.bytesurl(resp.getheader(r'content-type', r'')) |
350 proto = pycompat.bytesurl(resp.getheader(r'content-type', r'')) |
350 except AttributeError: |
351 except AttributeError: |
351 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r'')) |
352 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r'')) |
352 |
353 |
353 safeurl = util.hidepassword(baseurl) |
354 safeurl = util.hidepassword(baseurl) |
354 if proto.startswith('application/hg-error'): |
355 if proto.startswith(b'application/hg-error'): |
355 raise error.OutOfBandError(resp.read()) |
356 raise error.OutOfBandError(resp.read()) |
356 |
357 |
357 # Pre 1.0 versions of Mercurial used text/plain and |
358 # Pre 1.0 versions of Mercurial used text/plain and |
358 # application/hg-changegroup. We don't support such old servers. |
359 # application/hg-changegroup. We don't support such old servers. |
359 if not proto.startswith('application/mercurial-'): |
360 if not proto.startswith(b'application/mercurial-'): |
360 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl)) |
361 ui.debug(b"requested URL: '%s'\n" % util.hidepassword(requrl)) |
361 msg = _( |
362 msg = _( |
362 "'%s' does not appear to be an hg repository:\n" |
363 b"'%s' does not appear to be an hg repository:\n" |
363 "---%%<--- (%s)\n%s\n---%%<---\n" |
364 b"---%%<--- (%s)\n%s\n---%%<---\n" |
364 ) % (safeurl, proto or 'no content-type', resp.read(1024)) |
365 ) % (safeurl, proto or b'no content-type', resp.read(1024)) |
365 |
366 |
366 # Some servers may strip the query string from the redirect. We |
367 # Some servers may strip the query string from the redirect. We |
367 # raise a special error type so callers can react to this specially. |
368 # raise a special error type so callers can react to this specially. |
368 if redirected and qsdropped: |
369 if redirected and qsdropped: |
369 raise RedirectedRepoError(msg, respurl) |
370 raise RedirectedRepoError(msg, respurl) |
370 else: |
371 else: |
371 raise error.RepoError(msg) |
372 raise error.RepoError(msg) |
372 |
373 |
373 try: |
374 try: |
374 subtype = proto.split('-', 1)[1] |
375 subtype = proto.split(b'-', 1)[1] |
375 |
376 |
376 # Unless we end up supporting CBOR in the legacy wire protocol, |
377 # Unless we end up supporting CBOR in the legacy wire protocol, |
377 # this should ONLY be encountered for the initial capabilities |
378 # this should ONLY be encountered for the initial capabilities |
378 # request during handshake. |
379 # request during handshake. |
379 if subtype == 'cbor': |
380 if subtype == b'cbor': |
380 if allowcbor: |
381 if allowcbor: |
381 return respurl, proto, resp |
382 return respurl, proto, resp |
382 else: |
383 else: |
383 raise error.RepoError( |
384 raise error.RepoError( |
384 _('unexpected CBOR response from ' 'server') |
385 _(b'unexpected CBOR response from ' b'server') |
385 ) |
386 ) |
386 |
387 |
387 version_info = tuple([int(n) for n in subtype.split('.')]) |
388 version_info = tuple([int(n) for n in subtype.split(b'.')]) |
388 except ValueError: |
389 except ValueError: |
389 raise error.RepoError( |
390 raise error.RepoError( |
390 _("'%s' sent a broken Content-Type " "header (%s)") |
391 _(b"'%s' sent a broken Content-Type " b"header (%s)") |
391 % (safeurl, proto) |
392 % (safeurl, proto) |
392 ) |
393 ) |
393 |
394 |
394 # TODO consider switching to a decompression reader that uses |
395 # TODO consider switching to a decompression reader that uses |
395 # generators. |
396 # generators. |
396 if version_info == (0, 1): |
397 if version_info == (0, 1): |
397 if compressible: |
398 if compressible: |
398 resp = util.compengines['zlib'].decompressorreader(resp) |
399 resp = util.compengines[b'zlib'].decompressorreader(resp) |
399 |
400 |
400 elif version_info == (0, 2): |
401 elif version_info == (0, 2): |
401 # application/mercurial-0.2 always identifies the compression |
402 # application/mercurial-0.2 always identifies the compression |
402 # engine in the payload header. |
403 # engine in the payload header. |
403 elen = struct.unpack('B', util.readexactly(resp, 1))[0] |
404 elen = struct.unpack(b'B', util.readexactly(resp, 1))[0] |
404 ename = util.readexactly(resp, elen) |
405 ename = util.readexactly(resp, elen) |
405 engine = util.compengines.forwiretype(ename) |
406 engine = util.compengines.forwiretype(ename) |
406 |
407 |
407 resp = engine.decompressorreader(resp) |
408 resp = engine.decompressorreader(resp) |
408 else: |
409 else: |
409 raise error.RepoError( |
410 raise error.RepoError( |
410 _("'%s' uses newer protocol %s") % (safeurl, subtype) |
411 _(b"'%s' uses newer protocol %s") % (safeurl, subtype) |
411 ) |
412 ) |
412 |
413 |
413 return respurl, proto, resp |
414 return respurl, proto, resp |
414 |
415 |
415 |
416 |
499 |
500 |
500 def _callpush(self, cmd, cg, **args): |
501 def _callpush(self, cmd, cg, **args): |
501 # have to stream bundle to a temp file because we do not have |
502 # have to stream bundle to a temp file because we do not have |
502 # http 1.1 chunked transfer. |
503 # http 1.1 chunked transfer. |
503 |
504 |
504 types = self.capable('unbundle') |
505 types = self.capable(b'unbundle') |
505 try: |
506 try: |
506 types = types.split(',') |
507 types = types.split(b',') |
507 except AttributeError: |
508 except AttributeError: |
508 # servers older than d1b16a746db6 will send 'unbundle' as a |
509 # servers older than d1b16a746db6 will send 'unbundle' as a |
509 # boolean capability. They only support headerless/uncompressed |
510 # boolean capability. They only support headerless/uncompressed |
510 # bundles. |
511 # bundles. |
511 types = [""] |
512 types = [b""] |
512 for x in types: |
513 for x in types: |
513 if x in bundle2.bundletypes: |
514 if x in bundle2.bundletypes: |
514 type = x |
515 type = x |
515 break |
516 break |
516 |
517 |
517 tempname = bundle2.writebundle(self.ui, cg, None, type) |
518 tempname = bundle2.writebundle(self.ui, cg, None, type) |
518 fp = httpconnection.httpsendfile(self.ui, tempname, "rb") |
519 fp = httpconnection.httpsendfile(self.ui, tempname, b"rb") |
519 headers = {r'Content-Type': r'application/mercurial-0.1'} |
520 headers = {r'Content-Type': r'application/mercurial-0.1'} |
520 |
521 |
521 try: |
522 try: |
522 r = self._call(cmd, data=fp, headers=headers, **args) |
523 r = self._call(cmd, data=fp, headers=headers, **args) |
523 vals = r.split('\n', 1) |
524 vals = r.split(b'\n', 1) |
524 if len(vals) < 2: |
525 if len(vals) < 2: |
525 raise error.ResponseError(_("unexpected response:"), r) |
526 raise error.ResponseError(_(b"unexpected response:"), r) |
526 return vals |
527 return vals |
527 except urlerr.httperror: |
528 except urlerr.httperror: |
528 # Catch and re-raise these so we don't try and treat them |
529 # Catch and re-raise these so we don't try and treat them |
529 # like generic socket errors. They lack any values in |
530 # like generic socket errors. They lack any values in |
530 # .args on Python 3 which breaks our socket.error block. |
531 # .args on Python 3 which breaks our socket.error block. |
531 raise |
532 raise |
532 except socket.error as err: |
533 except socket.error as err: |
533 if err.args[0] in (errno.ECONNRESET, errno.EPIPE): |
534 if err.args[0] in (errno.ECONNRESET, errno.EPIPE): |
534 raise error.Abort(_('push failed: %s') % err.args[1]) |
535 raise error.Abort(_(b'push failed: %s') % err.args[1]) |
535 raise error.Abort(err.args[1]) |
536 raise error.Abort(err.args[1]) |
536 finally: |
537 finally: |
537 fp.close() |
538 fp.close() |
538 os.unlink(tempname) |
539 os.unlink(tempname) |
539 |
540 |
540 def _calltwowaystream(self, cmd, fp, **args): |
541 def _calltwowaystream(self, cmd, fp, **args): |
541 filename = None |
542 filename = None |
542 try: |
543 try: |
543 # dump bundle to disk |
544 # dump bundle to disk |
544 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg") |
545 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg") |
545 with os.fdopen(fd, r"wb") as fh: |
546 with os.fdopen(fd, r"wb") as fh: |
546 d = fp.read(4096) |
547 d = fp.read(4096) |
547 while d: |
548 while d: |
548 fh.write(d) |
549 fh.write(d) |
549 d = fp.read(4096) |
550 d = fp.read(4096) |
550 # start http push |
551 # start http push |
551 with httpconnection.httpsendfile(self.ui, filename, "rb") as fp_: |
552 with httpconnection.httpsendfile(self.ui, filename, b"rb") as fp_: |
552 headers = {r'Content-Type': r'application/mercurial-0.1'} |
553 headers = {r'Content-Type': r'application/mercurial-0.1'} |
553 return self._callstream(cmd, data=fp_, headers=headers, **args) |
554 return self._callstream(cmd, data=fp_, headers=headers, **args) |
554 finally: |
555 finally: |
555 if filename is not None: |
556 if filename is not None: |
556 os.unlink(filename) |
557 os.unlink(filename) |
596 |
597 |
597 handler = wireprotov2peer.clienthandler( |
598 handler = wireprotov2peer.clienthandler( |
598 ui, reactor, opener=opener, requestbuilder=requestbuilder |
599 ui, reactor, opener=opener, requestbuilder=requestbuilder |
599 ) |
600 ) |
600 |
601 |
601 url = '%s/%s' % (apiurl, permission) |
602 url = b'%s/%s' % (apiurl, permission) |
602 |
603 |
603 if len(requests) > 1: |
604 if len(requests) > 1: |
604 url += '/multirequest' |
605 url += b'/multirequest' |
605 else: |
606 else: |
606 url += '/%s' % requests[0][0] |
607 url += b'/%s' % requests[0][0] |
607 |
608 |
608 ui.debug('sending %d commands\n' % len(requests)) |
609 ui.debug(b'sending %d commands\n' % len(requests)) |
609 for command, args, f in requests: |
610 for command, args, f in requests: |
610 ui.debug( |
611 ui.debug( |
611 'sending command %s: %s\n' |
612 b'sending command %s: %s\n' |
612 % (command, stringutil.pprint(args, indent=2)) |
613 % (command, stringutil.pprint(args, indent=2)) |
613 ) |
614 ) |
614 assert not list( |
615 assert not list( |
615 handler.callcommand(command, args, f, redirect=redirect) |
616 handler.callcommand(command, args, f, redirect=redirect) |
616 ) |
617 ) |
681 self.close() |
682 self.close() |
682 |
683 |
683 def callcommand(self, command, args): |
684 def callcommand(self, command, args): |
684 if self._sent: |
685 if self._sent: |
685 raise error.ProgrammingError( |
686 raise error.ProgrammingError( |
686 'callcommand() cannot be used after ' 'commands are sent' |
687 b'callcommand() cannot be used after ' b'commands are sent' |
687 ) |
688 ) |
688 |
689 |
689 if self._closed: |
690 if self._closed: |
690 raise error.ProgrammingError( |
691 raise error.ProgrammingError( |
691 'callcommand() cannot be used after ' 'close()' |
692 b'callcommand() cannot be used after ' b'close()' |
692 ) |
693 ) |
693 |
694 |
694 # The service advertises which commands are available. So if we attempt |
695 # The service advertises which commands are available. So if we attempt |
695 # to call an unknown command or pass an unknown argument, we can screen |
696 # to call an unknown command or pass an unknown argument, we can screen |
696 # for this. |
697 # for this. |
697 if command not in self._descriptor['commands']: |
698 if command not in self._descriptor[b'commands']: |
698 raise error.ProgrammingError( |
699 raise error.ProgrammingError( |
699 'wire protocol command %s is not available' % command |
700 b'wire protocol command %s is not available' % command |
700 ) |
701 ) |
701 |
702 |
702 cmdinfo = self._descriptor['commands'][command] |
703 cmdinfo = self._descriptor[b'commands'][command] |
703 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {})) |
704 unknownargs = set(args.keys()) - set(cmdinfo.get(b'args', {})) |
704 |
705 |
705 if unknownargs: |
706 if unknownargs: |
706 raise error.ProgrammingError( |
707 raise error.ProgrammingError( |
707 'wire protocol command %s does not accept argument: %s' |
708 b'wire protocol command %s does not accept argument: %s' |
708 % (command, ', '.join(sorted(unknownargs))) |
709 % (command, b', '.join(sorted(unknownargs))) |
709 ) |
710 ) |
710 |
711 |
711 self._neededpermissions |= set(cmdinfo['permissions']) |
712 self._neededpermissions |= set(cmdinfo[b'permissions']) |
712 |
713 |
713 # TODO we /could/ also validate types here, since the API descriptor |
714 # TODO we /could/ also validate types here, since the API descriptor |
714 # includes types... |
715 # includes types... |
715 |
716 |
716 f = pycompat.futures.Future() |
717 f = pycompat.futures.Future() |
754 if not calls: |
755 if not calls: |
755 return |
756 return |
756 |
757 |
757 permissions = set(self._neededpermissions) |
758 permissions = set(self._neededpermissions) |
758 |
759 |
759 if 'push' in permissions and 'pull' in permissions: |
760 if b'push' in permissions and b'pull' in permissions: |
760 permissions.remove('pull') |
761 permissions.remove(b'pull') |
761 |
762 |
762 if len(permissions) > 1: |
763 if len(permissions) > 1: |
763 raise error.RepoError( |
764 raise error.RepoError( |
764 _('cannot make request requiring multiple ' 'permissions: %s') |
765 _(b'cannot make request requiring multiple ' b'permissions: %s') |
765 % _(', ').join(sorted(permissions)) |
766 % _(b', ').join(sorted(permissions)) |
766 ) |
767 ) |
767 |
768 |
768 permission = {'push': 'rw', 'pull': 'ro',}[permissions.pop()] |
769 permission = {b'push': b'rw', b'pull': b'ro',}[permissions.pop()] |
769 |
770 |
770 handler, resp = sendv2request( |
771 handler, resp = sendv2request( |
771 self._ui, |
772 self._ui, |
772 self._opener, |
773 self._opener, |
773 self._requestbuilder, |
774 self._requestbuilder, |
830 self, ui, repourl, apipath, opener, requestbuilder, apidescriptor |
831 self, ui, repourl, apipath, opener, requestbuilder, apidescriptor |
831 ): |
832 ): |
832 self.ui = ui |
833 self.ui = ui |
833 self.apidescriptor = apidescriptor |
834 self.apidescriptor = apidescriptor |
834 |
835 |
835 if repourl.endswith('/'): |
836 if repourl.endswith(b'/'): |
836 repourl = repourl[:-1] |
837 repourl = repourl[:-1] |
837 |
838 |
838 self._url = repourl |
839 self._url = repourl |
839 self._apipath = apipath |
840 self._apipath = apipath |
840 self._apiurl = '%s/%s' % (repourl, apipath) |
841 self._apiurl = b'%s/%s' % (repourl, apipath) |
841 self._opener = opener |
842 self._opener = opener |
842 self._requestbuilder = requestbuilder |
843 self._requestbuilder = requestbuilder |
843 |
844 |
844 self._redirect = wireprotov2peer.supportedredirects(ui, apidescriptor) |
845 self._redirect = wireprotov2peer.supportedredirects(ui, apidescriptor) |
845 |
846 |
879 # The capabilities used internally historically map to capabilities |
880 # The capabilities used internally historically map to capabilities |
880 # advertised from the "capabilities" wire protocol command. However, |
881 # advertised from the "capabilities" wire protocol command. However, |
881 # version 2 of that command works differently. |
882 # version 2 of that command works differently. |
882 |
883 |
883 # Maps to commands that are available. |
884 # Maps to commands that are available. |
884 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'): |
885 if name in ( |
|
886 b'branchmap', |
|
887 b'getbundle', |
|
888 b'known', |
|
889 b'lookup', |
|
890 b'pushkey', |
|
891 ): |
885 return True |
892 return True |
886 |
893 |
887 # Other concepts. |
894 # Other concepts. |
888 if name in 'bundle2': |
895 if name in b'bundle2': |
889 return True |
896 return True |
890 |
897 |
891 # Alias command-* to presence of command of that name. |
898 # Alias command-* to presence of command of that name. |
892 if name.startswith('command-'): |
899 if name.startswith(b'command-'): |
893 return name[len('command-') :] in self.apidescriptor['commands'] |
900 return name[len(b'command-') :] in self.apidescriptor[b'commands'] |
894 |
901 |
895 return False |
902 return False |
896 |
903 |
897 def requirecap(self, name, purpose): |
904 def requirecap(self, name, purpose): |
898 if self.capable(name): |
905 if self.capable(name): |
899 return |
906 return |
900 |
907 |
901 raise error.CapabilityError( |
908 raise error.CapabilityError( |
902 _( |
909 _( |
903 'cannot %s; client or remote repository does not support the ' |
910 b'cannot %s; client or remote repository does not support the ' |
904 '\'%s\' capability' |
911 b'\'%s\' capability' |
905 ) |
912 ) |
906 % (purpose, name) |
913 % (purpose, name) |
907 ) |
914 ) |
908 |
915 |
909 # End of ipeercapabilities. |
916 # End of ipeercapabilities. |
933 # |
940 # |
934 # priority |
941 # priority |
935 # Integer priority for the service. If we could choose from multiple |
942 # Integer priority for the service. If we could choose from multiple |
936 # services, we choose the one with the highest priority. |
943 # services, we choose the one with the highest priority. |
937 API_PEERS = { |
944 API_PEERS = { |
938 wireprototypes.HTTP_WIREPROTO_V2: {'init': httpv2peer, 'priority': 50,}, |
945 wireprototypes.HTTP_WIREPROTO_V2: {b'init': httpv2peer, b'priority': 50,}, |
939 } |
946 } |
940 |
947 |
941 |
948 |
942 def performhandshake(ui, url, opener, requestbuilder): |
949 def performhandshake(ui, url, opener, requestbuilder): |
943 # The handshake is a request to the capabilities command. |
950 # The handshake is a request to the capabilities command. |
944 |
951 |
945 caps = None |
952 caps = None |
946 |
953 |
947 def capable(x): |
954 def capable(x): |
948 raise error.ProgrammingError('should not be called') |
955 raise error.ProgrammingError(b'should not be called') |
949 |
956 |
950 args = {} |
957 args = {} |
951 |
958 |
952 # The client advertises support for newer protocols by adding an |
959 # The client advertises support for newer protocols by adding an |
953 # X-HgUpgrade-* header with a list of supported APIs and an |
960 # X-HgUpgrade-* header with a list of supported APIs and an |
954 # X-HgProto-* header advertising which serializing formats it supports. |
961 # X-HgProto-* header advertising which serializing formats it supports. |
955 # We only support the HTTP version 2 transport and CBOR responses for |
962 # We only support the HTTP version 2 transport and CBOR responses for |
956 # now. |
963 # now. |
957 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2') |
964 advertisev2 = ui.configbool(b'experimental', b'httppeer.advertise-v2') |
958 |
965 |
959 if advertisev2: |
966 if advertisev2: |
960 args['headers'] = { |
967 args[b'headers'] = { |
961 r'X-HgProto-1': r'cbor', |
968 r'X-HgProto-1': r'cbor', |
962 } |
969 } |
963 |
970 |
964 args['headers'].update( |
971 args[b'headers'].update( |
965 encodevalueinheaders( |
972 encodevalueinheaders( |
966 ' '.join(sorted(API_PEERS)), |
973 b' '.join(sorted(API_PEERS)), |
967 'X-HgUpgrade', |
974 b'X-HgUpgrade', |
968 # We don't know the header limit this early. |
975 # We don't know the header limit this early. |
969 # So make it small. |
976 # So make it small. |
970 1024, |
977 1024, |
971 ) |
978 ) |
972 ) |
979 ) |
973 |
980 |
974 req, requrl, qs = makev1commandrequest( |
981 req, requrl, qs = makev1commandrequest( |
975 ui, requestbuilder, caps, capable, url, 'capabilities', args |
982 ui, requestbuilder, caps, capable, url, b'capabilities', args |
976 ) |
983 ) |
977 resp = sendrequest(ui, opener, req) |
984 resp = sendrequest(ui, opener, req) |
978 |
985 |
979 # The server may redirect us to the repo root, stripping the |
986 # The server may redirect us to the repo root, stripping the |
980 # ?cmd=capabilities query string from the URL. The server would likely |
987 # ?cmd=capabilities query string from the URL. The server would likely |
992 respurl, ct, resp = parsev1commandresponse( |
999 respurl, ct, resp = parsev1commandresponse( |
993 ui, url, requrl, qs, resp, compressible=False, allowcbor=advertisev2 |
1000 ui, url, requrl, qs, resp, compressible=False, allowcbor=advertisev2 |
994 ) |
1001 ) |
995 except RedirectedRepoError as e: |
1002 except RedirectedRepoError as e: |
996 req, requrl, qs = makev1commandrequest( |
1003 req, requrl, qs = makev1commandrequest( |
997 ui, requestbuilder, caps, capable, e.respurl, 'capabilities', args |
1004 ui, requestbuilder, caps, capable, e.respurl, b'capabilities', args |
998 ) |
1005 ) |
999 resp = sendrequest(ui, opener, req) |
1006 resp = sendrequest(ui, opener, req) |
1000 respurl, ct, resp = parsev1commandresponse( |
1007 respurl, ct, resp = parsev1commandresponse( |
1001 ui, url, requrl, qs, resp, compressible=False, allowcbor=advertisev2 |
1008 ui, url, requrl, qs, resp, compressible=False, allowcbor=advertisev2 |
1002 ) |
1009 ) |
1004 try: |
1011 try: |
1005 rawdata = resp.read() |
1012 rawdata = resp.read() |
1006 finally: |
1013 finally: |
1007 resp.close() |
1014 resp.close() |
1008 |
1015 |
1009 if not ct.startswith('application/mercurial-'): |
1016 if not ct.startswith(b'application/mercurial-'): |
1010 raise error.ProgrammingError('unexpected content-type: %s' % ct) |
1017 raise error.ProgrammingError(b'unexpected content-type: %s' % ct) |
1011 |
1018 |
1012 if advertisev2: |
1019 if advertisev2: |
1013 if ct == 'application/mercurial-cbor': |
1020 if ct == b'application/mercurial-cbor': |
1014 try: |
1021 try: |
1015 info = cborutil.decodeall(rawdata)[0] |
1022 info = cborutil.decodeall(rawdata)[0] |
1016 except cborutil.CBORDecodeError: |
1023 except cborutil.CBORDecodeError: |
1017 raise error.Abort( |
1024 raise error.Abort( |
1018 _('error decoding CBOR from remote server'), |
1025 _(b'error decoding CBOR from remote server'), |
1019 hint=_( |
1026 hint=_( |
1020 'try again and consider contacting ' |
1027 b'try again and consider contacting ' |
1021 'the server operator' |
1028 b'the server operator' |
1022 ), |
1029 ), |
1023 ) |
1030 ) |
1024 |
1031 |
1025 # We got a legacy response. That's fine. |
1032 # We got a legacy response. That's fine. |
1026 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'): |
1033 elif ct in (b'application/mercurial-0.1', b'application/mercurial-0.2'): |
1027 info = {'v1capabilities': set(rawdata.split())} |
1034 info = {b'v1capabilities': set(rawdata.split())} |
1028 |
1035 |
1029 else: |
1036 else: |
1030 raise error.RepoError( |
1037 raise error.RepoError( |
1031 _('unexpected response type from server: %s') % ct |
1038 _(b'unexpected response type from server: %s') % ct |
1032 ) |
1039 ) |
1033 else: |
1040 else: |
1034 info = {'v1capabilities': set(rawdata.split())} |
1041 info = {b'v1capabilities': set(rawdata.split())} |
1035 |
1042 |
1036 return respurl, info |
1043 return respurl, info |
1037 |
1044 |
1038 |
1045 |
1039 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request): |
1046 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request): |
1066 # example, the caller could say "I want a peer that does X." It's quite |
1073 # example, the caller could say "I want a peer that does X." It's quite |
1067 # possible that not all peers would do that. Since we know the service |
1074 # possible that not all peers would do that. Since we know the service |
1068 # capabilities, we could filter out services not meeting the |
1075 # capabilities, we could filter out services not meeting the |
1069 # requirements. Possibly by consulting the interfaces defined by the |
1076 # requirements. Possibly by consulting the interfaces defined by the |
1070 # peer type. |
1077 # peer type. |
1071 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys()) |
1078 apipeerchoices = set(info.get(b'apis', {}).keys()) & set(API_PEERS.keys()) |
1072 |
1079 |
1073 preferredchoices = sorted( |
1080 preferredchoices = sorted( |
1074 apipeerchoices, key=lambda x: API_PEERS[x]['priority'], reverse=True |
1081 apipeerchoices, key=lambda x: API_PEERS[x][b'priority'], reverse=True |
1075 ) |
1082 ) |
1076 |
1083 |
1077 for service in preferredchoices: |
1084 for service in preferredchoices: |
1078 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service) |
1085 apipath = b'%s/%s' % (info[b'apibase'].rstrip(b'/'), service) |
1079 |
1086 |
1080 return API_PEERS[service]['init']( |
1087 return API_PEERS[service][b'init']( |
1081 ui, respurl, apipath, opener, requestbuilder, info['apis'][service] |
1088 ui, respurl, apipath, opener, requestbuilder, info[b'apis'][service] |
1082 ) |
1089 ) |
1083 |
1090 |
1084 # Failed to construct an API peer. Fall back to legacy. |
1091 # Failed to construct an API peer. Fall back to legacy. |
1085 return httppeer( |
1092 return httppeer( |
1086 ui, path, respurl, opener, requestbuilder, info['v1capabilities'] |
1093 ui, path, respurl, opener, requestbuilder, info[b'v1capabilities'] |
1087 ) |
1094 ) |
1088 |
1095 |
1089 |
1096 |
1090 def instance(ui, path, create, intents=None, createopts=None): |
1097 def instance(ui, path, create, intents=None, createopts=None): |
1091 if create: |
1098 if create: |
1092 raise error.Abort(_('cannot create new http repository')) |
1099 raise error.Abort(_(b'cannot create new http repository')) |
1093 try: |
1100 try: |
1094 if path.startswith('https:') and not urlmod.has_https: |
1101 if path.startswith(b'https:') and not urlmod.has_https: |
1095 raise error.Abort( |
1102 raise error.Abort( |
1096 _('Python support for SSL and HTTPS ' 'is not installed') |
1103 _(b'Python support for SSL and HTTPS ' b'is not installed') |
1097 ) |
1104 ) |
1098 |
1105 |
1099 inst = makepeer(ui, path) |
1106 inst = makepeer(ui, path) |
1100 |
1107 |
1101 return inst |
1108 return inst |
1102 except error.RepoError as httpexception: |
1109 except error.RepoError as httpexception: |
1103 try: |
1110 try: |
1104 r = statichttprepo.instance(ui, "static-" + path, create) |
1111 r = statichttprepo.instance(ui, b"static-" + path, create) |
1105 ui.note(_('(falling back to static-http)\n')) |
1112 ui.note(_(b'(falling back to static-http)\n')) |
1106 return r |
1113 return r |
1107 except error.RepoError: |
1114 except error.RepoError: |
1108 raise httpexception # use the original http RepoError instead |
1115 raise httpexception # use the original http RepoError instead |