54 |
54 |
55 # Root URL does nothing meaningful... yet. |
55 # Root URL does nothing meaningful... yet. |
56 if not urlparts: |
56 if not urlparts: |
57 res.status = b'200 OK' |
57 res.status = b'200 OK' |
58 res.headers[b'Content-Type'] = b'text/plain' |
58 res.headers[b'Content-Type'] = b'text/plain' |
59 res.setbodybytes(_('HTTP version 2 API handler')) |
59 res.setbodybytes(_(b'HTTP version 2 API handler')) |
60 return |
60 return |
61 |
61 |
62 if len(urlparts) == 1: |
62 if len(urlparts) == 1: |
63 res.status = b'404 Not Found' |
63 res.status = b'404 Not Found' |
64 res.headers[b'Content-Type'] = b'text/plain' |
64 res.headers[b'Content-Type'] = b'text/plain' |
65 res.setbodybytes( |
65 res.setbodybytes( |
66 _('do not know how to process %s\n') % req.dispatchpath |
66 _(b'do not know how to process %s\n') % req.dispatchpath |
67 ) |
67 ) |
68 return |
68 return |
69 |
69 |
70 permission, command = urlparts[0:2] |
70 permission, command = urlparts[0:2] |
71 |
71 |
72 if permission not in (b'ro', b'rw'): |
72 if permission not in (b'ro', b'rw'): |
73 res.status = b'404 Not Found' |
73 res.status = b'404 Not Found' |
74 res.headers[b'Content-Type'] = b'text/plain' |
74 res.headers[b'Content-Type'] = b'text/plain' |
75 res.setbodybytes(_('unknown permission: %s') % permission) |
75 res.setbodybytes(_(b'unknown permission: %s') % permission) |
76 return |
76 return |
77 |
77 |
78 if req.method != 'POST': |
78 if req.method != b'POST': |
79 res.status = b'405 Method Not Allowed' |
79 res.status = b'405 Method Not Allowed' |
80 res.headers[b'Allow'] = b'POST' |
80 res.headers[b'Allow'] = b'POST' |
81 res.setbodybytes(_('commands require POST requests')) |
81 res.setbodybytes(_(b'commands require POST requests')) |
82 return |
82 return |
83 |
83 |
84 # At some point we'll want to use our own API instead of recycling the |
84 # At some point we'll want to use our own API instead of recycling the |
85 # behavior of version 1 of the wire protocol... |
85 # behavior of version 1 of the wire protocol... |
86 # TODO return reasonable responses - not responses that overload the |
86 # TODO return reasonable responses - not responses that overload the |
87 # HTTP status line message for error reporting. |
87 # HTTP status line message for error reporting. |
88 try: |
88 try: |
89 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push') |
89 checkperm(rctx, req, b'pull' if permission == b'ro' else b'push') |
90 except hgwebcommon.ErrorResponse as e: |
90 except hgwebcommon.ErrorResponse as e: |
91 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e)) |
91 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e)) |
92 for k, v in e.headers: |
92 for k, v in e.headers: |
93 res.headers[k] = v |
93 res.headers[k] = v |
94 res.setbodybytes('permission denied') |
94 res.setbodybytes(b'permission denied') |
95 return |
95 return |
96 |
96 |
97 # We have a special endpoint to reflect the request back at the client. |
97 # We have a special endpoint to reflect the request back at the client. |
98 if command == b'debugreflect': |
98 if command == b'debugreflect': |
99 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res) |
99 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res) |
100 return |
100 return |
101 |
101 |
102 # Extra commands that we handle that aren't really wire protocol |
102 # Extra commands that we handle that aren't really wire protocol |
103 # commands. Think extra hard before making this hackery available to |
103 # commands. Think extra hard before making this hackery available to |
104 # extension. |
104 # extension. |
105 extracommands = {'multirequest'} |
105 extracommands = {b'multirequest'} |
106 |
106 |
107 if command not in COMMANDS and command not in extracommands: |
107 if command not in COMMANDS and command not in extracommands: |
108 res.status = b'404 Not Found' |
108 res.status = b'404 Not Found' |
109 res.headers[b'Content-Type'] = b'text/plain' |
109 res.headers[b'Content-Type'] = b'text/plain' |
110 res.setbodybytes(_('unknown wire protocol command: %s\n') % command) |
110 res.setbodybytes(_(b'unknown wire protocol command: %s\n') % command) |
111 return |
111 return |
112 |
112 |
113 repo = rctx.repo |
113 repo = rctx.repo |
114 ui = repo.ui |
114 ui = repo.ui |
115 |
115 |
119 not COMMANDS.commandavailable(command, proto) |
119 not COMMANDS.commandavailable(command, proto) |
120 and command not in extracommands |
120 and command not in extracommands |
121 ): |
121 ): |
122 res.status = b'404 Not Found' |
122 res.status = b'404 Not Found' |
123 res.headers[b'Content-Type'] = b'text/plain' |
123 res.headers[b'Content-Type'] = b'text/plain' |
124 res.setbodybytes(_('invalid wire protocol command: %s') % command) |
124 res.setbodybytes(_(b'invalid wire protocol command: %s') % command) |
125 return |
125 return |
126 |
126 |
127 # TODO consider cases where proxies may add additional Accept headers. |
127 # TODO consider cases where proxies may add additional Accept headers. |
128 if req.headers.get(b'Accept') != FRAMINGTYPE: |
128 if req.headers.get(b'Accept') != FRAMINGTYPE: |
129 res.status = b'406 Not Acceptable' |
129 res.status = b'406 Not Acceptable' |
130 res.headers[b'Content-Type'] = b'text/plain' |
130 res.headers[b'Content-Type'] = b'text/plain' |
131 res.setbodybytes( |
131 res.setbodybytes( |
132 _('client MUST specify Accept header with value: %s\n') |
132 _(b'client MUST specify Accept header with value: %s\n') |
133 % FRAMINGTYPE |
133 % FRAMINGTYPE |
134 ) |
134 ) |
135 return |
135 return |
136 |
136 |
137 if req.headers.get(b'Content-Type') != FRAMINGTYPE: |
137 if req.headers.get(b'Content-Type') != FRAMINGTYPE: |
138 res.status = b'415 Unsupported Media Type' |
138 res.status = b'415 Unsupported Media Type' |
139 # TODO we should send a response with appropriate media type, |
139 # TODO we should send a response with appropriate media type, |
140 # since client does Accept it. |
140 # since client does Accept it. |
141 res.headers[b'Content-Type'] = b'text/plain' |
141 res.headers[b'Content-Type'] = b'text/plain' |
142 res.setbodybytes( |
142 res.setbodybytes( |
143 _('client MUST send Content-Type header with ' 'value: %s\n') |
143 _(b'client MUST send Content-Type header with ' b'value: %s\n') |
144 % FRAMINGTYPE |
144 % FRAMINGTYPE |
145 ) |
145 ) |
146 return |
146 return |
147 |
147 |
148 _processhttpv2request(ui, repo, req, res, permission, command, proto) |
148 _processhttpv2request(ui, repo, req, res, permission, command, proto) |
158 tracker. We then dump the log of all that activity back out to the |
158 tracker. We then dump the log of all that activity back out to the |
159 client. |
159 client. |
160 """ |
160 """ |
161 # Reflection APIs have a history of being abused, accidentally disclosing |
161 # Reflection APIs have a history of being abused, accidentally disclosing |
162 # sensitive data, etc. So we have a config knob. |
162 # sensitive data, etc. So we have a config knob. |
163 if not ui.configbool('experimental', 'web.api.debugreflect'): |
163 if not ui.configbool(b'experimental', b'web.api.debugreflect'): |
164 res.status = b'404 Not Found' |
164 res.status = b'404 Not Found' |
165 res.headers[b'Content-Type'] = b'text/plain' |
165 res.headers[b'Content-Type'] = b'text/plain' |
166 res.setbodybytes(_('debugreflect service not available')) |
166 res.setbodybytes(_(b'debugreflect service not available')) |
167 return |
167 return |
168 |
168 |
169 # We assume we have a unified framing protocol request body. |
169 # We assume we have a unified framing protocol request body. |
170 |
170 |
171 reactor = wireprotoframing.serverreactor(ui) |
171 reactor = wireprotoframing.serverreactor(ui) |
185 |
185 |
186 action, meta = reactor.onframerecv(frame) |
186 action, meta = reactor.onframerecv(frame) |
187 states.append(templatefilters.json((action, meta))) |
187 states.append(templatefilters.json((action, meta))) |
188 |
188 |
189 action, meta = reactor.oninputeof() |
189 action, meta = reactor.oninputeof() |
190 meta['action'] = action |
190 meta[b'action'] = action |
191 states.append(templatefilters.json(meta)) |
191 states.append(templatefilters.json(meta)) |
192 |
192 |
193 res.status = b'200 OK' |
193 res.status = b'200 OK' |
194 res.headers[b'Content-Type'] = b'text/plain' |
194 res.headers[b'Content-Type'] = b'text/plain' |
195 res.setbodybytes(b'\n'.join(states)) |
195 res.setbodybytes(b'\n'.join(states)) |
241 if sentoutput: |
241 if sentoutput: |
242 return |
242 return |
243 |
243 |
244 seencommand = True |
244 seencommand = True |
245 |
245 |
246 elif action == 'error': |
246 elif action == b'error': |
247 # TODO define proper error mechanism. |
247 # TODO define proper error mechanism. |
248 res.status = b'200 OK' |
248 res.status = b'200 OK' |
249 res.headers[b'Content-Type'] = b'text/plain' |
249 res.headers[b'Content-Type'] = b'text/plain' |
250 res.setbodybytes(meta['message'] + b'\n') |
250 res.setbodybytes(meta[b'message'] + b'\n') |
251 return |
251 return |
252 else: |
252 else: |
253 raise error.ProgrammingError( |
253 raise error.ProgrammingError( |
254 'unhandled action from frame processor: %s' % action |
254 b'unhandled action from frame processor: %s' % action |
255 ) |
255 ) |
256 |
256 |
257 action, meta = reactor.oninputeof() |
257 action, meta = reactor.oninputeof() |
258 if action == 'sendframes': |
258 if action == b'sendframes': |
259 # We assume we haven't started sending the response yet. If we're |
259 # We assume we haven't started sending the response yet. If we're |
260 # wrong, the response type will raise an exception. |
260 # wrong, the response type will raise an exception. |
261 res.status = b'200 OK' |
261 res.status = b'200 OK' |
262 res.headers[b'Content-Type'] = FRAMINGTYPE |
262 res.headers[b'Content-Type'] = FRAMINGTYPE |
263 res.setbodygen(meta['framegen']) |
263 res.setbodygen(meta[b'framegen']) |
264 elif action == 'noop': |
264 elif action == b'noop': |
265 pass |
265 pass |
266 else: |
266 else: |
267 raise error.ProgrammingError( |
267 raise error.ProgrammingError( |
268 'unhandled action from frame processor: %s' % action |
268 b'unhandled action from frame processor: %s' % action |
269 ) |
269 ) |
270 |
270 |
271 |
271 |
272 def _httpv2runcommand( |
272 def _httpv2runcommand( |
273 ui, |
273 ui, |
299 # execute multiple commands. We double check permissions of each command |
299 # execute multiple commands. We double check permissions of each command |
300 # as it is invoked to ensure there is no privilege escalation. |
300 # as it is invoked to ensure there is no privilege escalation. |
301 # TODO consider allowing multiple commands to regular command URLs |
301 # TODO consider allowing multiple commands to regular command URLs |
302 # iff each command is the same. |
302 # iff each command is the same. |
303 |
303 |
304 proto = httpv2protocolhandler(req, ui, args=command['args']) |
304 proto = httpv2protocolhandler(req, ui, args=command[b'args']) |
305 |
305 |
306 if reqcommand == b'multirequest': |
306 if reqcommand == b'multirequest': |
307 if not COMMANDS.commandavailable(command['command'], proto): |
307 if not COMMANDS.commandavailable(command[b'command'], proto): |
308 # TODO proper error mechanism |
308 # TODO proper error mechanism |
309 res.status = b'200 OK' |
309 res.status = b'200 OK' |
310 res.headers[b'Content-Type'] = b'text/plain' |
310 res.headers[b'Content-Type'] = b'text/plain' |
311 res.setbodybytes( |
311 res.setbodybytes( |
312 _('wire protocol command not available: %s') |
312 _(b'wire protocol command not available: %s') |
313 % command['command'] |
313 % command[b'command'] |
314 ) |
314 ) |
315 return True |
315 return True |
316 |
316 |
317 # TODO don't use assert here, since it may be elided by -O. |
317 # TODO don't use assert here, since it may be elided by -O. |
318 assert authedperm in (b'ro', b'rw') |
318 assert authedperm in (b'ro', b'rw') |
319 wirecommand = COMMANDS[command['command']] |
319 wirecommand = COMMANDS[command[b'command']] |
320 assert wirecommand.permission in ('push', 'pull') |
320 assert wirecommand.permission in (b'push', b'pull') |
321 |
321 |
322 if authedperm == b'ro' and wirecommand.permission != 'pull': |
322 if authedperm == b'ro' and wirecommand.permission != b'pull': |
323 # TODO proper error mechanism |
323 # TODO proper error mechanism |
324 res.status = b'403 Forbidden' |
324 res.status = b'403 Forbidden' |
325 res.headers[b'Content-Type'] = b'text/plain' |
325 res.headers[b'Content-Type'] = b'text/plain' |
326 res.setbodybytes( |
326 res.setbodybytes( |
327 _('insufficient permissions to execute ' 'command: %s') |
327 _(b'insufficient permissions to execute ' b'command: %s') |
328 % command['command'] |
328 % command[b'command'] |
329 ) |
329 ) |
330 return True |
330 return True |
331 |
331 |
332 # TODO should we also call checkperm() here? Maybe not if we're going |
332 # TODO should we also call checkperm() here? Maybe not if we're going |
333 # to overhaul that API. The granted scope from the URL check should |
333 # to overhaul that API. The granted scope from the URL check should |
338 if issubsequent: |
338 if issubsequent: |
339 # TODO proper error mechanism |
339 # TODO proper error mechanism |
340 res.status = b'200 OK' |
340 res.status = b'200 OK' |
341 res.headers[b'Content-Type'] = b'text/plain' |
341 res.headers[b'Content-Type'] = b'text/plain' |
342 res.setbodybytes( |
342 res.setbodybytes( |
343 _('multiple commands cannot be issued to this ' 'URL') |
343 _(b'multiple commands cannot be issued to this ' b'URL') |
344 ) |
344 ) |
345 return True |
345 return True |
346 |
346 |
347 if reqcommand != command['command']: |
347 if reqcommand != command[b'command']: |
348 # TODO define proper error mechanism |
348 # TODO define proper error mechanism |
349 res.status = b'200 OK' |
349 res.status = b'200 OK' |
350 res.headers[b'Content-Type'] = b'text/plain' |
350 res.headers[b'Content-Type'] = b'text/plain' |
351 res.setbodybytes(_('command in frame must match command in URL')) |
351 res.setbodybytes(_(b'command in frame must match command in URL')) |
352 return True |
352 return True |
353 |
353 |
354 res.status = b'200 OK' |
354 res.status = b'200 OK' |
355 res.headers[b'Content-Type'] = FRAMINGTYPE |
355 res.headers[b'Content-Type'] = FRAMINGTYPE |
356 |
356 |
357 try: |
357 try: |
358 objs = dispatch(repo, proto, command['command'], command['redirect']) |
358 objs = dispatch(repo, proto, command[b'command'], command[b'redirect']) |
359 |
359 |
360 action, meta = reactor.oncommandresponsereadyobjects( |
360 action, meta = reactor.oncommandresponsereadyobjects( |
361 outstream, command['requestid'], objs |
361 outstream, command[b'requestid'], objs |
362 ) |
362 ) |
363 |
363 |
364 except error.WireprotoCommandError as e: |
364 except error.WireprotoCommandError as e: |
365 action, meta = reactor.oncommanderror( |
365 action, meta = reactor.oncommanderror( |
366 outstream, command['requestid'], e.message, e.messageargs |
366 outstream, command[b'requestid'], e.message, e.messageargs |
367 ) |
367 ) |
368 |
368 |
369 except Exception as e: |
369 except Exception as e: |
370 action, meta = reactor.onservererror( |
370 action, meta = reactor.onservererror( |
371 outstream, |
371 outstream, |
372 command['requestid'], |
372 command[b'requestid'], |
373 _('exception when invoking command: %s') |
373 _(b'exception when invoking command: %s') |
374 % stringutil.forcebytestr(e), |
374 % stringutil.forcebytestr(e), |
375 ) |
375 ) |
376 |
376 |
377 if action == 'sendframes': |
377 if action == b'sendframes': |
378 res.setbodygen(meta['framegen']) |
378 res.setbodygen(meta[b'framegen']) |
379 return True |
379 return True |
380 elif action == 'noop': |
380 elif action == b'noop': |
381 return False |
381 return False |
382 else: |
382 else: |
383 raise error.ProgrammingError( |
383 raise error.ProgrammingError( |
384 'unhandled event from reactor: %s' % action |
384 b'unhandled event from reactor: %s' % action |
385 ) |
385 ) |
386 |
386 |
387 |
387 |
388 def getdispatchrepo(repo, proto, command): |
388 def getdispatchrepo(repo, proto, command): |
389 viewconfig = repo.ui.config('server', 'view') |
389 viewconfig = repo.ui.config(b'server', b'view') |
390 return repo.filtered(viewconfig) |
390 return repo.filtered(viewconfig) |
391 |
391 |
392 |
392 |
393 def dispatch(repo, proto, command, redirect): |
393 def dispatch(repo, proto, command, redirect): |
394 """Run a wire protocol command. |
394 """Run a wire protocol command. |
482 # First look for args that were passed but aren't registered on this |
482 # First look for args that were passed but aren't registered on this |
483 # command. |
483 # command. |
484 extra = set(self._args) - set(args) |
484 extra = set(self._args) - set(args) |
485 if extra: |
485 if extra: |
486 raise error.WireprotoCommandError( |
486 raise error.WireprotoCommandError( |
487 'unsupported argument to command: %s' % ', '.join(sorted(extra)) |
487 b'unsupported argument to command: %s' |
|
488 % b', '.join(sorted(extra)) |
488 ) |
489 ) |
489 |
490 |
490 # And look for required arguments that are missing. |
491 # And look for required arguments that are missing. |
491 missing = {a for a in args if args[a]['required']} - set(self._args) |
492 missing = {a for a in args if args[a][b'required']} - set(self._args) |
492 |
493 |
493 if missing: |
494 if missing: |
494 raise error.WireprotoCommandError( |
495 raise error.WireprotoCommandError( |
495 'missing required arguments: %s' % ', '.join(sorted(missing)) |
496 b'missing required arguments: %s' % b', '.join(sorted(missing)) |
496 ) |
497 ) |
497 |
498 |
498 # Now derive the arguments to pass to the command, taking into |
499 # Now derive the arguments to pass to the command, taking into |
499 # account the arguments specified by the client. |
500 # account the arguments specified by the client. |
500 data = {} |
501 data = {} |
501 for k, meta in sorted(args.items()): |
502 for k, meta in sorted(args.items()): |
502 # This argument wasn't passed by the client. |
503 # This argument wasn't passed by the client. |
503 if k not in self._args: |
504 if k not in self._args: |
504 data[k] = meta['default']() |
505 data[k] = meta[b'default']() |
505 continue |
506 continue |
506 |
507 |
507 v = self._args[k] |
508 v = self._args[k] |
508 |
509 |
509 # Sets may be expressed as lists. Silently normalize. |
510 # Sets may be expressed as lists. Silently normalize. |
510 if meta['type'] == 'set' and isinstance(v, list): |
511 if meta[b'type'] == b'set' and isinstance(v, list): |
511 v = set(v) |
512 v = set(v) |
512 |
513 |
513 # TODO consider more/stronger type validation. |
514 # TODO consider more/stronger type validation. |
514 |
515 |
515 data[k] = v |
516 data[k] = v |
548 |
549 |
549 These capabilities are distinct from the capabilities for version 1 |
550 These capabilities are distinct from the capabilities for version 1 |
550 transports. |
551 transports. |
551 """ |
552 """ |
552 caps = { |
553 caps = { |
553 'commands': {}, |
554 b'commands': {}, |
554 'framingmediatypes': [FRAMINGTYPE], |
555 b'framingmediatypes': [FRAMINGTYPE], |
555 'pathfilterprefixes': set(narrowspec.VALID_PREFIXES), |
556 b'pathfilterprefixes': set(narrowspec.VALID_PREFIXES), |
556 } |
557 } |
557 |
558 |
558 for command, entry in COMMANDS.items(): |
559 for command, entry in COMMANDS.items(): |
559 args = {} |
560 args = {} |
560 |
561 |
561 for arg, meta in entry.args.items(): |
562 for arg, meta in entry.args.items(): |
562 args[arg] = { |
563 args[arg] = { |
563 # TODO should this be a normalized type using CBOR's |
564 # TODO should this be a normalized type using CBOR's |
564 # terminology? |
565 # terminology? |
565 b'type': meta['type'], |
566 b'type': meta[b'type'], |
566 b'required': meta['required'], |
567 b'required': meta[b'required'], |
567 } |
568 } |
568 |
569 |
569 if not meta['required']: |
570 if not meta[b'required']: |
570 args[arg][b'default'] = meta['default']() |
571 args[arg][b'default'] = meta[b'default']() |
571 |
572 |
572 if meta['validvalues']: |
573 if meta[b'validvalues']: |
573 args[arg][b'validvalues'] = meta['validvalues'] |
574 args[arg][b'validvalues'] = meta[b'validvalues'] |
574 |
575 |
575 # TODO this type of check should be defined in a per-command callback. |
576 # TODO this type of check should be defined in a per-command callback. |
576 if ( |
577 if ( |
577 command == b'rawstorefiledata' |
578 command == b'rawstorefiledata' |
578 and not streamclone.allowservergeneration(repo) |
579 and not streamclone.allowservergeneration(repo) |
579 ): |
580 ): |
580 continue |
581 continue |
581 |
582 |
582 caps['commands'][command] = { |
583 caps[b'commands'][command] = { |
583 'args': args, |
584 b'args': args, |
584 'permissions': [entry.permission], |
585 b'permissions': [entry.permission], |
585 } |
586 } |
586 |
587 |
587 if entry.extracapabilitiesfn: |
588 if entry.extracapabilitiesfn: |
588 extracaps = entry.extracapabilitiesfn(repo, proto) |
589 extracaps = entry.extracapabilitiesfn(repo, proto) |
589 caps['commands'][command].update(extracaps) |
590 caps[b'commands'][command].update(extracaps) |
590 |
591 |
591 caps['rawrepoformats'] = sorted(repo.requirements & repo.supportedformats) |
592 caps[b'rawrepoformats'] = sorted(repo.requirements & repo.supportedformats) |
592 |
593 |
593 targets = getadvertisedredirecttargets(repo, proto) |
594 targets = getadvertisedredirecttargets(repo, proto) |
594 if targets: |
595 if targets: |
595 caps[b'redirect'] = { |
596 caps[b'redirect'] = { |
596 b'targets': [], |
597 b'targets': [], |
597 b'hashes': [b'sha256', b'sha1'], |
598 b'hashes': [b'sha256', b'sha1'], |
598 } |
599 } |
599 |
600 |
600 for target in targets: |
601 for target in targets: |
601 entry = { |
602 entry = { |
602 b'name': target['name'], |
603 b'name': target[b'name'], |
603 b'protocol': target['protocol'], |
604 b'protocol': target[b'protocol'], |
604 b'uris': target['uris'], |
605 b'uris': target[b'uris'], |
605 } |
606 } |
606 |
607 |
607 for key in ('snirequired', 'tlsversions'): |
608 for key in (b'snirequired', b'tlsversions'): |
608 if key in target: |
609 if key in target: |
609 entry[key] = target[key] |
610 entry[key] = target[key] |
610 |
611 |
611 caps[b'redirect'][b'targets'].append(entry) |
612 caps[b'redirect'][b'targets'].append(entry) |
612 |
613 |
708 argument containing the active cacher for the request and returns a bytes |
709 argument containing the active cacher for the request and returns a bytes |
709 containing the key in a cache the response to this command may be cached |
710 containing the key in a cache the response to this command may be cached |
710 under. |
711 under. |
711 """ |
712 """ |
712 transports = { |
713 transports = { |
713 k for k, v in wireprototypes.TRANSPORTS.items() if v['version'] == 2 |
714 k for k, v in wireprototypes.TRANSPORTS.items() if v[b'version'] == 2 |
714 } |
715 } |
715 |
716 |
716 if permission not in ('push', 'pull'): |
717 if permission not in (b'push', b'pull'): |
717 raise error.ProgrammingError( |
718 raise error.ProgrammingError( |
718 'invalid wire protocol permission; ' |
719 b'invalid wire protocol permission; ' |
719 'got %s; expected "push" or "pull"' % permission |
720 b'got %s; expected "push" or "pull"' % permission |
720 ) |
721 ) |
721 |
722 |
722 if args is None: |
723 if args is None: |
723 args = {} |
724 args = {} |
724 |
725 |
725 if not isinstance(args, dict): |
726 if not isinstance(args, dict): |
726 raise error.ProgrammingError( |
727 raise error.ProgrammingError( |
727 'arguments for version 2 commands ' 'must be declared as dicts' |
728 b'arguments for version 2 commands ' b'must be declared as dicts' |
728 ) |
729 ) |
729 |
730 |
730 for arg, meta in args.items(): |
731 for arg, meta in args.items(): |
731 if arg == '*': |
732 if arg == b'*': |
732 raise error.ProgrammingError( |
733 raise error.ProgrammingError( |
733 '* argument name not allowed on ' 'version 2 commands' |
734 b'* argument name not allowed on ' b'version 2 commands' |
734 ) |
735 ) |
735 |
736 |
736 if not isinstance(meta, dict): |
737 if not isinstance(meta, dict): |
737 raise error.ProgrammingError( |
738 raise error.ProgrammingError( |
738 'arguments for version 2 commands ' |
739 b'arguments for version 2 commands ' |
739 'must declare metadata as a dict' |
740 b'must declare metadata as a dict' |
740 ) |
741 ) |
741 |
742 |
742 if 'type' not in meta: |
743 if b'type' not in meta: |
743 raise error.ProgrammingError( |
744 raise error.ProgrammingError( |
744 '%s argument for command %s does not ' |
745 b'%s argument for command %s does not ' |
745 'declare type field' % (arg, name) |
746 b'declare type field' % (arg, name) |
746 ) |
747 ) |
747 |
748 |
748 if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'): |
749 if meta[b'type'] not in ( |
|
750 b'bytes', |
|
751 b'int', |
|
752 b'list', |
|
753 b'dict', |
|
754 b'set', |
|
755 b'bool', |
|
756 ): |
749 raise error.ProgrammingError( |
757 raise error.ProgrammingError( |
750 '%s argument for command %s has ' |
758 b'%s argument for command %s has ' |
751 'illegal type: %s' % (arg, name, meta['type']) |
759 b'illegal type: %s' % (arg, name, meta[b'type']) |
752 ) |
760 ) |
753 |
761 |
754 if 'example' not in meta: |
762 if b'example' not in meta: |
755 raise error.ProgrammingError( |
763 raise error.ProgrammingError( |
756 '%s argument for command %s does not ' |
764 b'%s argument for command %s does not ' |
757 'declare example field' % (arg, name) |
765 b'declare example field' % (arg, name) |
758 ) |
766 ) |
759 |
767 |
760 meta['required'] = 'default' not in meta |
768 meta[b'required'] = b'default' not in meta |
761 |
769 |
762 meta.setdefault('default', lambda: None) |
770 meta.setdefault(b'default', lambda: None) |
763 meta.setdefault('validvalues', None) |
771 meta.setdefault(b'validvalues', None) |
764 |
772 |
765 def register(func): |
773 def register(func): |
766 if name in COMMANDS: |
774 if name in COMMANDS: |
767 raise error.ProgrammingError( |
775 raise error.ProgrammingError( |
768 '%s command already registered ' 'for version 2' % name |
776 b'%s command already registered ' b'for version 2' % name |
769 ) |
777 ) |
770 |
778 |
771 COMMANDS[name] = wireprototypes.commandentry( |
779 COMMANDS[name] = wireprototypes.commandentry( |
772 func, |
780 func, |
773 args=args, |
781 args=args, |
794 * The media type used. |
802 * The media type used. |
795 * Wire protocol version string. |
803 * Wire protocol version string. |
796 * The repository path. |
804 * The repository path. |
797 """ |
805 """ |
798 if not allargs: |
806 if not allargs: |
799 raise error.ProgrammingError('only allargs=True is currently supported') |
807 raise error.ProgrammingError( |
|
808 b'only allargs=True is currently supported' |
|
809 ) |
800 |
810 |
801 if localversion is None: |
811 if localversion is None: |
802 raise error.ProgrammingError('must set localversion argument value') |
812 raise error.ProgrammingError(b'must set localversion argument value') |
803 |
813 |
804 def cachekeyfn(repo, proto, cacher, **args): |
814 def cachekeyfn(repo, proto, cacher, **args): |
805 spec = COMMANDS[command] |
815 spec = COMMANDS[command] |
806 |
816 |
807 # Commands that mutate the repo can not be cached. |
817 # Commands that mutate the repo can not be cached. |
808 if spec.permission == 'push': |
818 if spec.permission == b'push': |
809 return None |
819 return None |
810 |
820 |
811 # TODO config option to disable caching. |
821 # TODO config option to disable caching. |
812 |
822 |
813 # Our key derivation strategy is to construct a data structure |
823 # Our key derivation strategy is to construct a data structure |
878 seen = set() |
888 seen = set() |
879 nodes = [] |
889 nodes = [] |
880 |
890 |
881 if not isinstance(revisions, list): |
891 if not isinstance(revisions, list): |
882 raise error.WireprotoCommandError( |
892 raise error.WireprotoCommandError( |
883 'revisions must be defined as an ' 'array' |
893 b'revisions must be defined as an ' b'array' |
884 ) |
894 ) |
885 |
895 |
886 for spec in revisions: |
896 for spec in revisions: |
887 if b'type' not in spec: |
897 if b'type' not in spec: |
888 raise error.WireprotoCommandError( |
898 raise error.WireprotoCommandError( |
889 'type key not present in revision specifier' |
899 b'type key not present in revision specifier' |
890 ) |
900 ) |
891 |
901 |
892 typ = spec[b'type'] |
902 typ = spec[b'type'] |
893 |
903 |
894 if typ == b'changesetexplicit': |
904 if typ == b'changesetexplicit': |
895 if b'nodes' not in spec: |
905 if b'nodes' not in spec: |
896 raise error.WireprotoCommandError( |
906 raise error.WireprotoCommandError( |
897 'nodes key not present in changesetexplicit revision ' |
907 b'nodes key not present in changesetexplicit revision ' |
898 'specifier' |
908 b'specifier' |
899 ) |
909 ) |
900 |
910 |
901 for node in spec[b'nodes']: |
911 for node in spec[b'nodes']: |
902 if node not in seen: |
912 if node not in seen: |
903 nodes.append(node) |
913 nodes.append(node) |
945 nodes.append(n) |
955 nodes.append(n) |
946 seen.add(n) |
956 seen.add(n) |
947 |
957 |
948 else: |
958 else: |
949 raise error.WireprotoCommandError( |
959 raise error.WireprotoCommandError( |
950 'unknown revision specifier type: %s', (typ,) |
960 b'unknown revision specifier type: %s', (typ,) |
951 ) |
961 ) |
952 |
962 |
953 return nodes |
963 return nodes |
954 |
964 |
955 |
965 |
956 @wireprotocommand('branchmap', permission='pull') |
966 @wireprotocommand(b'branchmap', permission=b'pull') |
957 def branchmapv2(repo, proto): |
967 def branchmapv2(repo, proto): |
958 yield {encoding.fromlocal(k): v for k, v in repo.branchmap().iteritems()} |
968 yield {encoding.fromlocal(k): v for k, v in repo.branchmap().iteritems()} |
959 |
969 |
960 |
970 |
961 @wireprotocommand('capabilities', permission='pull') |
971 @wireprotocommand(b'capabilities', permission=b'pull') |
962 def capabilitiesv2(repo, proto): |
972 def capabilitiesv2(repo, proto): |
963 yield _capabilitiesv2(repo, proto) |
973 yield _capabilitiesv2(repo, proto) |
964 |
974 |
965 |
975 |
966 @wireprotocommand( |
976 @wireprotocommand( |
967 'changesetdata', |
977 b'changesetdata', |
968 args={ |
978 args={ |
969 'revisions': { |
979 b'revisions': { |
970 'type': 'list', |
980 b'type': b'list', |
971 'example': [ |
981 b'example': [ |
972 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],} |
982 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],} |
973 ], |
983 ], |
974 }, |
984 }, |
975 'fields': { |
985 b'fields': { |
976 'type': 'set', |
986 b'type': b'set', |
977 'default': set, |
987 b'default': set, |
978 'example': {b'parents', b'revision'}, |
988 b'example': {b'parents', b'revision'}, |
979 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'}, |
989 b'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'}, |
980 }, |
990 }, |
981 }, |
991 }, |
982 permission='pull', |
992 permission=b'pull', |
983 ) |
993 ) |
984 def changesetdata(repo, proto, revisions, fields): |
994 def changesetdata(repo, proto, revisions, fields): |
985 # TODO look for unknown fields and abort when they can't be serviced. |
995 # TODO look for unknown fields and abort when they can't be serviced. |
986 # This could probably be validated by dispatcher using validvalues. |
996 # This could probably be validated by dispatcher using validvalues. |
987 |
997 |
988 cl = repo.changelog |
998 cl = repo.changelog |
989 outgoing = resolvenodes(repo, revisions) |
999 outgoing = resolvenodes(repo, revisions) |
990 publishing = repo.publishing() |
1000 publishing = repo.publishing() |
991 |
1001 |
992 if outgoing: |
1002 if outgoing: |
993 repo.hook('preoutgoing', throw=True, source='serve') |
1003 repo.hook(b'preoutgoing', throw=True, source=b'serve') |
994 |
1004 |
995 yield { |
1005 yield { |
996 b'totalitems': len(outgoing), |
1006 b'totalitems': len(outgoing), |
997 } |
1007 } |
998 |
1008 |
1144 # filter those out. |
1154 # filter those out. |
1145 return repo.narrowmatch(matcher) |
1155 return repo.narrowmatch(matcher) |
1146 |
1156 |
1147 |
1157 |
1148 @wireprotocommand( |
1158 @wireprotocommand( |
1149 'filedata', |
1159 b'filedata', |
1150 args={ |
1160 args={ |
1151 'haveparents': { |
1161 b'haveparents': { |
1152 'type': 'bool', |
1162 b'type': b'bool', |
1153 'default': lambda: False, |
1163 b'default': lambda: False, |
1154 'example': True, |
1164 b'example': True, |
1155 }, |
1165 }, |
1156 'nodes': {'type': 'list', 'example': [b'0123456...'],}, |
1166 b'nodes': {b'type': b'list', b'example': [b'0123456...'],}, |
1157 'fields': { |
1167 b'fields': { |
1158 'type': 'set', |
1168 b'type': b'set', |
1159 'default': set, |
1169 b'default': set, |
1160 'example': {b'parents', b'revision'}, |
1170 b'example': {b'parents', b'revision'}, |
1161 'validvalues': {b'parents', b'revision', b'linknode'}, |
1171 b'validvalues': {b'parents', b'revision', b'linknode'}, |
1162 }, |
1172 }, |
1163 'path': {'type': 'bytes', 'example': b'foo.txt',}, |
1173 b'path': {b'type': b'bytes', b'example': b'foo.txt',}, |
1164 }, |
1174 }, |
1165 permission='pull', |
1175 permission=b'pull', |
1166 # TODO censoring a file revision won't invalidate the cache. |
1176 # TODO censoring a file revision won't invalidate the cache. |
1167 # Figure out a way to take censoring into account when deriving |
1177 # Figure out a way to take censoring into account when deriving |
1168 # the cache key. |
1178 # the cache key. |
1169 cachekeyfn=makecommandcachekeyfn('filedata', 1, allargs=True), |
1179 cachekeyfn=makecommandcachekeyfn(b'filedata', 1, allargs=True), |
1170 ) |
1180 ) |
1171 def filedata(repo, proto, haveparents, nodes, fields, path): |
1181 def filedata(repo, proto, haveparents, nodes, fields, path): |
1172 # TODO this API allows access to file revisions that are attached to |
1182 # TODO this API allows access to file revisions that are attached to |
1173 # secret changesets. filesdata does not have this problem. Maybe this |
1183 # secret changesets. filesdata does not have this problem. Maybe this |
1174 # API should be deleted? |
1184 # API should be deleted? |
1221 b'recommendedbatchsize': batchsize, |
1231 b'recommendedbatchsize': batchsize, |
1222 } |
1232 } |
1223 |
1233 |
1224 |
1234 |
1225 @wireprotocommand( |
1235 @wireprotocommand( |
1226 'filesdata', |
1236 b'filesdata', |
1227 args={ |
1237 args={ |
1228 'haveparents': { |
1238 b'haveparents': { |
1229 'type': 'bool', |
1239 b'type': b'bool', |
1230 'default': lambda: False, |
1240 b'default': lambda: False, |
1231 'example': True, |
1241 b'example': True, |
1232 }, |
1242 }, |
1233 'fields': { |
1243 b'fields': { |
1234 'type': 'set', |
1244 b'type': b'set', |
1235 'default': set, |
1245 b'default': set, |
1236 'example': {b'parents', b'revision'}, |
1246 b'example': {b'parents', b'revision'}, |
1237 'validvalues': { |
1247 b'validvalues': { |
1238 b'firstchangeset', |
1248 b'firstchangeset', |
1239 b'linknode', |
1249 b'linknode', |
1240 b'parents', |
1250 b'parents', |
1241 b'revision', |
1251 b'revision', |
1242 }, |
1252 }, |
1243 }, |
1253 }, |
1244 'pathfilter': { |
1254 b'pathfilter': { |
1245 'type': 'dict', |
1255 b'type': b'dict', |
1246 'default': lambda: None, |
1256 b'default': lambda: None, |
1247 'example': {b'include': [b'path:tests']}, |
1257 b'example': {b'include': [b'path:tests']}, |
1248 }, |
1258 }, |
1249 'revisions': { |
1259 b'revisions': { |
1250 'type': 'list', |
1260 b'type': b'list', |
1251 'example': [ |
1261 b'example': [ |
1252 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],} |
1262 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],} |
1253 ], |
1263 ], |
1254 }, |
1264 }, |
1255 }, |
1265 }, |
1256 permission='pull', |
1266 permission=b'pull', |
1257 # TODO censoring a file revision won't invalidate the cache. |
1267 # TODO censoring a file revision won't invalidate the cache. |
1258 # Figure out a way to take censoring into account when deriving |
1268 # Figure out a way to take censoring into account when deriving |
1259 # the cache key. |
1269 # the cache key. |
1260 cachekeyfn=makecommandcachekeyfn('filesdata', 1, allargs=True), |
1270 cachekeyfn=makecommandcachekeyfn(b'filesdata', 1, allargs=True), |
1261 extracapabilitiesfn=filesdatacapabilities, |
1271 extracapabilitiesfn=filesdatacapabilities, |
1262 ) |
1272 ) |
1263 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions): |
1273 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions): |
1264 # TODO This should operate on a repo that exposes obsolete changesets. There |
1274 # TODO This should operate on a repo that exposes obsolete changesets. There |
1265 # is a race between a client making a push that obsoletes a changeset and |
1275 # is a race between a client making a push that obsoletes a changeset and |
1325 for o in emitfilerevisions(repo, path, revisions, filenodes, fields): |
1335 for o in emitfilerevisions(repo, path, revisions, filenodes, fields): |
1326 yield o |
1336 yield o |
1327 |
1337 |
1328 |
1338 |
1329 @wireprotocommand( |
1339 @wireprotocommand( |
1330 'heads', |
1340 b'heads', |
1331 args={ |
1341 args={ |
1332 'publiconly': { |
1342 b'publiconly': { |
1333 'type': 'bool', |
1343 b'type': b'bool', |
1334 'default': lambda: False, |
1344 b'default': lambda: False, |
1335 'example': False, |
1345 b'example': False, |
1336 }, |
1346 }, |
1337 }, |
1347 }, |
1338 permission='pull', |
1348 permission=b'pull', |
1339 ) |
1349 ) |
1340 def headsv2(repo, proto, publiconly): |
1350 def headsv2(repo, proto, publiconly): |
1341 if publiconly: |
1351 if publiconly: |
1342 repo = repo.filtered('immutable') |
1352 repo = repo.filtered(b'immutable') |
1343 |
1353 |
1344 yield repo.heads() |
1354 yield repo.heads() |
1345 |
1355 |
1346 |
1356 |
1347 @wireprotocommand( |
1357 @wireprotocommand( |
1348 'known', |
1358 b'known', |
1349 args={ |
1359 args={ |
1350 'nodes': {'type': 'list', 'default': list, 'example': [b'deadbeef'],}, |
1360 b'nodes': { |
|
1361 b'type': b'list', |
|
1362 b'default': list, |
|
1363 b'example': [b'deadbeef'], |
|
1364 }, |
1351 }, |
1365 }, |
1352 permission='pull', |
1366 permission=b'pull', |
1353 ) |
1367 ) |
1354 def knownv2(repo, proto, nodes): |
1368 def knownv2(repo, proto, nodes): |
1355 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes)) |
1369 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes)) |
1356 yield result |
1370 yield result |
1357 |
1371 |
1358 |
1372 |
1359 @wireprotocommand( |
1373 @wireprotocommand( |
1360 'listkeys', |
1374 b'listkeys', |
1361 args={'namespace': {'type': 'bytes', 'example': b'ns',},}, |
1375 args={b'namespace': {b'type': b'bytes', b'example': b'ns',},}, |
1362 permission='pull', |
1376 permission=b'pull', |
1363 ) |
1377 ) |
1364 def listkeysv2(repo, proto, namespace): |
1378 def listkeysv2(repo, proto, namespace): |
1365 keys = repo.listkeys(encoding.tolocal(namespace)) |
1379 keys = repo.listkeys(encoding.tolocal(namespace)) |
1366 keys = { |
1380 keys = { |
1367 encoding.fromlocal(k): encoding.fromlocal(v) |
1381 encoding.fromlocal(k): encoding.fromlocal(v) |
1394 b'recommendedbatchsize': batchsize, |
1408 b'recommendedbatchsize': batchsize, |
1395 } |
1409 } |
1396 |
1410 |
1397 |
1411 |
1398 @wireprotocommand( |
1412 @wireprotocommand( |
1399 'manifestdata', |
1413 b'manifestdata', |
1400 args={ |
1414 args={ |
1401 'nodes': {'type': 'list', 'example': [b'0123456...'],}, |
1415 b'nodes': {b'type': b'list', b'example': [b'0123456...'],}, |
1402 'haveparents': { |
1416 b'haveparents': { |
1403 'type': 'bool', |
1417 b'type': b'bool', |
1404 'default': lambda: False, |
1418 b'default': lambda: False, |
1405 'example': True, |
1419 b'example': True, |
1406 }, |
1420 }, |
1407 'fields': { |
1421 b'fields': { |
1408 'type': 'set', |
1422 b'type': b'set', |
1409 'default': set, |
1423 b'default': set, |
1410 'example': {b'parents', b'revision'}, |
1424 b'example': {b'parents', b'revision'}, |
1411 'validvalues': {b'parents', b'revision'}, |
1425 b'validvalues': {b'parents', b'revision'}, |
1412 }, |
1426 }, |
1413 'tree': {'type': 'bytes', 'example': b'',}, |
1427 b'tree': {b'type': b'bytes', b'example': b'',}, |
1414 }, |
1428 }, |
1415 permission='pull', |
1429 permission=b'pull', |
1416 cachekeyfn=makecommandcachekeyfn('manifestdata', 1, allargs=True), |
1430 cachekeyfn=makecommandcachekeyfn(b'manifestdata', 1, allargs=True), |
1417 extracapabilitiesfn=manifestdatacapabilities, |
1431 extracapabilitiesfn=manifestdatacapabilities, |
1418 ) |
1432 ) |
1419 def manifestdata(repo, proto, haveparents, nodes, fields, tree): |
1433 def manifestdata(repo, proto, haveparents, nodes, fields, tree): |
1420 store = repo.manifestlog.getstorage(tree) |
1434 store = repo.manifestlog.getstorage(tree) |
1421 |
1435 |
1422 # Validate the node is known and abort on unknown revisions. |
1436 # Validate the node is known and abort on unknown revisions. |
1423 for node in nodes: |
1437 for node in nodes: |
1424 try: |
1438 try: |
1425 store.rev(node) |
1439 store.rev(node) |
1426 except error.LookupError: |
1440 except error.LookupError: |
1427 raise error.WireprotoCommandError('unknown node: %s', (node,)) |
1441 raise error.WireprotoCommandError(b'unknown node: %s', (node,)) |
1428 |
1442 |
1429 revisions = store.emitrevisions( |
1443 revisions = store.emitrevisions( |
1430 nodes, |
1444 nodes, |
1431 revisiondata=b'revision' in fields, |
1445 revisiondata=b'revision' in fields, |
1432 assumehaveparentrevisions=haveparents, |
1446 assumehaveparentrevisions=haveparents, |
1464 for extra in followingdata: |
1478 for extra in followingdata: |
1465 yield extra |
1479 yield extra |
1466 |
1480 |
1467 |
1481 |
1468 @wireprotocommand( |
1482 @wireprotocommand( |
1469 'pushkey', |
1483 b'pushkey', |
1470 args={ |
1484 args={ |
1471 'namespace': {'type': 'bytes', 'example': b'ns',}, |
1485 b'namespace': {b'type': b'bytes', b'example': b'ns',}, |
1472 'key': {'type': 'bytes', 'example': b'key',}, |
1486 b'key': {b'type': b'bytes', b'example': b'key',}, |
1473 'old': {'type': 'bytes', 'example': b'old',}, |
1487 b'old': {b'type': b'bytes', b'example': b'old',}, |
1474 'new': {'type': 'bytes', 'example': 'new',}, |
1488 b'new': {b'type': b'bytes', b'example': b'new',}, |
1475 }, |
1489 }, |
1476 permission='push', |
1490 permission=b'push', |
1477 ) |
1491 ) |
1478 def pushkeyv2(repo, proto, namespace, key, old, new): |
1492 def pushkeyv2(repo, proto, namespace, key, old, new): |
1479 # TODO handle ui output redirection |
1493 # TODO handle ui output redirection |
1480 yield repo.pushkey( |
1494 yield repo.pushkey( |
1481 encoding.tolocal(namespace), |
1495 encoding.tolocal(namespace), |
1484 encoding.tolocal(new), |
1498 encoding.tolocal(new), |
1485 ) |
1499 ) |
1486 |
1500 |
1487 |
1501 |
1488 @wireprotocommand( |
1502 @wireprotocommand( |
1489 'rawstorefiledata', |
1503 b'rawstorefiledata', |
1490 args={ |
1504 args={ |
1491 'files': {'type': 'list', 'example': [b'changelog', b'manifestlog'],}, |
1505 b'files': { |
1492 'pathfilter': { |
1506 b'type': b'list', |
1493 'type': 'list', |
1507 b'example': [b'changelog', b'manifestlog'], |
1494 'default': lambda: None, |
1508 }, |
1495 'example': {b'include': [b'path:tests']}, |
1509 b'pathfilter': { |
|
1510 b'type': b'list', |
|
1511 b'default': lambda: None, |
|
1512 b'example': {b'include': [b'path:tests']}, |
1496 }, |
1513 }, |
1497 }, |
1514 }, |
1498 permission='pull', |
1515 permission=b'pull', |
1499 ) |
1516 ) |
1500 def rawstorefiledata(repo, proto, files, pathfilter): |
1517 def rawstorefiledata(repo, proto, files, pathfilter): |
1501 if not streamclone.allowservergeneration(repo): |
1518 if not streamclone.allowservergeneration(repo): |
1502 raise error.WireprotoCommandError(b'stream clone is disabled') |
1519 raise error.WireprotoCommandError(b'stream clone is disabled') |
1503 |
1520 |